Extensions
Extensions add domain-specific MCP tools to Kaimon by running as separate Julia processes that connect back through the Gate. Each extension is a Julia package with a kaimon.toml manifest that declares the tools it provides.
How Extensions Work
An extension is a Julia project that:
Defines handler functions for one or more tools.
Declares those tools in a
kaimon.tomlmanifest at the project root.Is registered in Kaimon's extension registry (
~/.config/kaimon/extensions.json).
When Kaimon starts (or when you manually start an extension), it spawns a Julia subprocess that activates the extension project, calls its tools function, and connects back via KaimonGate.serve(). The extension's tools appear in the MCP tool list under a namespace prefix (e.g., smlabnotes.search).
The kaimon.toml Manifest
Every extension project must have a kaimon.toml file in its root directory:
[extension]
namespace = "myext"
module = "MyExtension"
tools_function = "create_gate_tools"
description = "What this extension does."
shutdown_function = "cleanup" # optional
tui_file = "src/tui_panel.jl" # optional
julia_flags = ["-t4,1"] # optional
event_topics = ["breakpoint_hit"] # optional
[extension.env] # optional
JULIA_THREAD_SLEEP_THRESHOLD = "0"| Field | Required | Description |
|---|---|---|
namespace | Yes | Dot-prefix for all tool names (e.g., myext.tool_name) |
module | Yes | Julia module name to using |
tools_function | Yes | Exported function that returns Vector{GateTool} |
description | No | Human-readable summary for display in the TUI and extension_info |
shutdown_function | No | Exported no-arg function called before the extension process exits (5 s timeout) |
tui_file | No | Path to a lightweight TUI panel file (relative to project root). Press [u] on the extension in the Extensions tab to open it. |
julia_flags | No | Julia startup flags for the extension process (e.g., ["-t4,1", "--heap-size-hint=1G"]). Defaults to -t auto. |
event_topics | No | Stream channels to forward from gate sessions to the extension (e.g., ["breakpoint_hit"]). Requires an on_event(channel, data, session_name) function in the module. |
[extension.env] | No | Environment variables for the extension process, applied only where the parent has not already set them — so a value you export always wins. For settings a runtime reads once at startup that no flag exposes; see below. |
[extension.env]
Some runtime behaviour is only reachable through the environment, and only at process start. The case this exists for is JULIA_THREAD_SLEEP_THRESHOLD: a thread woken by an arriving task spins for 4 ms before parking, betting that more work is coming. That is right for a compute pool and wrong for an extension that relays I/O — messages arriving faster than the window expires leave every thread spinning between them. Slate's notebook hub sets it to 0 for that reason, which took one notebook streaming audio from 156% of a core to 22%.
Values are stringified, so PORT = 8080 reaches the process as "8080". Defaults never overwrite an existing variable, so exporting one yourself is always enough to override a manifest for a debugging run.
Extension Registry
The extension registry at ~/.config/kaimon/extensions.json tracks which extensions are available:
{
"extensions": [
{
"project_path": "/path/to/MyExtension.jl",
"enabled": true,
"auto_start": true
}
]
}| Field | Description |
|---|---|
project_path | Absolute path to the extension project directory |
enabled | Whether the extension can be started |
auto_start | If true and enabled, automatically spawn at Kaimon startup |
Manage the registry through the TUI Extensions tab or by editing the file directly.
Lifecycle
Extensions go through these states:
| State | Description |
|---|---|
:stopped | Not running |
:starting | Subprocess spawned, waiting for Gate connection |
:running | Connected and serving tools |
:crashed | Process exited unexpectedly |
If an extension crashes, Kaimon automatically restarts it with exponential backoff (5s, 10s, 30s, 60s delays).
Tools Stay Listed Across a Restart
Restarting an extension is routine while you develop one, and an extension's tools disappearing and reappearing reads to a connected agent as an unreliable MCP server. So a restart Kaimon initiated (the TUI's r, or manage_extension(action="restart")) holds the extension's tools in the list instead of unregistering them. No tools/list_changed is sent, and the agent never sees the toolset change.
A call that arrives while the extension is still coming up parks rather than failing, and proceeds as soon as the tools re-register. The hold re-arms while the extension is on its way back, so a long precompile does not end it early, up to a ceiling of 10 minutes. If the extension crashes during the restart, or does not return before that ceiling, the tools are unregistered as they always were.
Two settings tune the waits, in ~/.config/kaimon/config.json:
| Key | Default | Meaning |
|---|---|---|
extension_tool_hold_seconds | 45 | How long the tools stay listed while the extension is down. Raise it if an extension's precompile is slow. |
extension_tool_park_seconds | 40 | How long an individual tool call waits for the extension to come back before giving up. Keep it under the hold, so a parked call cannot outlive the hold that justifies it. |
KAIMON_TOOL_HOLD_SECONDS and KAIMON_TOOL_PARK_SECONDS override the config file.
Note
This applies to a restart Kaimon initiated. An extension that crashes on its own is unregistered as before, because nothing has promised it is coming back.
Graceful Shutdown
When an extension is stopped (via the TUI, a restart, or Kaimon exiting), the shutdown sequence is:
Gate receives shutdown signal.
If
shutdown_functionis declared, the hook is called with a 5-second timeout. Use this to flush state, close connections, or log a shutdown message.If the hook completes or times out, the process exits normally.
If the process does not exit, Kaimon sends
SIGTERM, thenSIGKILLafter a grace period.
Tool Namespacing
Extension tools are namespaced with the extension's namespace value to avoid collisions with built-in tools and other extensions:
Extension with
namespace = "smlabnotes"exporting tool"search"→ registered assmlabnotes.searchIf two sessions declare the same namespace, the second gets a suffix:
smlabnotes_2
When a gate session connects, the TUI registers its tools into the MCP server's tool registry and sends notifications/tools/list_changed so clients refresh their tool list. When the session disconnects, the tools are unregistered.
Tools whose name starts with __ are registered as internal: still callable (an extension can relay to them behind the scenes), but hidden from the agent-facing tools/list by default. See Internal (hidden) tools.
Managing Extensions in the TUI

The TUI has an Extensions tab (tab 8) with a two-pane layout:
Left pane — Extension list with status indicators and uptime
Right pane — Detail view showing namespace, module, project path, description, status, session key, tools, and recent errors
Key Reference
| Key | Action |
|---|---|
a | Add a new extension (enter project path) |
d | Remove selected extension |
e | Toggle enabled/disabled |
t | Toggle auto-start |
s | Start extension (if stopped) |
x | Stop extension (if running) |
r | Restart extension |
u | Open TUI panel (if tui_file defined) |
Enter | Expand detail view |
Esc | Close detail view / panel / cancel flow |
The extension_info Tool
AI agents can discover extensions and their tools programmatically:
extension_info()
# Lists all extensions with status and tool names
extension_info(name="smlabnotes")
# Detailed view: status, description, tools with parameter schemasThe manage_extension Tool
The same lifecycle controls as the TUI Extensions tab, exposed to MCP clients so an agent can manage extensions programmatically. Target an extension by its namespace (see extension_info):
manage_extension(name="smlabnotes", action="restart") # restart the process
manage_extension(name="smlabnotes", action="stop") # stop it
manage_extension(name="smlabnotes", action="start") # start it (when stopped)
manage_extension(name="smlabnotes", action="disable") # disallow running (also stops it); persisted
manage_extension(name="smlabnotes", action="enable") # re-allow running; persisted
manage_extension(name="smlabnotes", action="enable_auto_start") # auto-start with Kaimon; persisted
manage_extension(name="smlabnotes", action="disable_auto_start")Returns the extension's resulting {status, enabled, auto_start}. enable/disable and the auto_start actions are persisted to extensions.json.
Writing an Extension
Project Structure
A full-featured extension looks like this:
MyExtension.jl/
├── Project.toml
├── kaimon.toml
└── src/
├── MyExtension.jl # module with tools + shutdown hook
└── tui_panel.jl # optional TUI panelSee examples/HelloExtension.jl for a complete working example with tools, push-based panel updates, and a shutdown hook.
kaimon.toml
[extension]
namespace = "myext"
module = "MyExtension"
tools_function = "create_tools"
description = "What this extension does."
shutdown_function = "on_shutdown" # optional
tui_file = "src/tui_panel.jl" # optionalDefining Tools
The tools_function receives the GateTool type as its argument and returns a vector of tool instances. Each handler's type signature is reflected automatically to generate MCP JSON Schema — primitive types, enums, structs, Union{T, Nothing} (optional), and vectors are all supported.
module MyExtension
export create_tools, on_shutdown
function create_tools(GateTool::Type)
"""
greet(name::String, enthusiastic::Bool = false) -> String
Return a greeting for the given name.
"""
function greet(name::String, enthusiastic::Bool = false)::String
msg = enthusiastic ? "Hello, $(name)! 🎉" : "Hello, $(name)."
# Push state to TUI panel (see "TUI Panel Protocol" below)
Main.Kaimon.KaimonGate.push_panel("last_greeting", msg)
return msg
end
return [GateTool("greet", greet)]
end
function on_shutdown()
@info "MyExtension shutting down"
end
endRegistering
Add the extension through the TUI Extensions tab (a key) or add an entry to ~/.config/kaimon/extensions.json manually. Once registered, start it with s in the TUI or enable auto_start for automatic startup.
Accessing Kaimon Tools from Extensions
Extensions can call back into Kaimon's MCP tools via the service endpoint:
# In a tool handler (Kaimon is loaded at Main scope in the extension subprocess):
# Call any registered MCP tool
result = Main.Kaimon.KaimonGate.call_tool(:search_code, Dict{String,Any}(
"query" => "HTTP routing",
"limit" => "5",
))
# Discover available tools
tools = Main.Kaimon.KaimonGate.list_tools()This uses a ZMQ REQ/REP connection to the Kaimon server's service socket, allowing extensions to compose with built-in tools and other extensions.
TUI Panel Protocol
Extensions can provide a TUI panel that renders inside Kaimon's Extensions tab. Set tui_file in kaimon.toml to point at a Julia file that defines these functions:
| Function | Required | Signature | Description |
|---|---|---|---|
init | Yes | (ctx) → state | Create initial panel state |
update! | No | (state, ctx) | Called each frame (~60 fps); read pushed data here |
view | Yes | (state, area::Rect, buf::Buffer) | Render into a Tachikoma buffer region |
handle_key! | No | (state, evt::KeyEvent) → Bool | Process input; return true if consumed |
cleanup! | Yes | (state, ctx) | Tear down when panel closes |
Lifecycle
User presses
uon a running extension in the Extensions tabKaimon
include()s thetui_fileinto a fresh anonymous moduleinit(ctx)is called to create the panel stateEach frame:
update!(state, ctx)thenview(state, area, buf)Key events route to
handle_key!(state, evt)— returntrueto consumeWhen the user presses
Esc,cleanup!(state, ctx)is called and the panel closes
ExtPanelContext
The ctx argument passed to init, update!, and cleanup! provides:
| Field | Type | Description |
|---|---|---|
session_key | String | 8-char gate session key for this extension |
tick | Int | Frame counter (increments each frame) |
_cache | Dict{Symbol,Any} | Scratch space; :panel_state is auto-populated by push_panel |
eval | Function | eval(code::String) → NamedTuple — evaluate code in the extension process |
request | Function | request(tool, args) → String — call a tool on the extension |
Key Handling with @match
Use the @match macro (from Match.jl, a Kaimon dependency) for clean dispatch:
using Match
function handle_key!(state, evt::Tachikoma.KeyEvent)::Bool
@match (evt.key, evt.char) begin
(:tab, _) => (state.selected = mod1(state.selected + 1, 3); true)
(:char, 'g') => begin do_greet!(state); true end
(:char, 'r') => begin do_roll!(state); true end
_ => false
end
endKaimonGate.push_panel() — Extension to Panel Communication
Instead of polling the extension process with ctx.eval(), tool handlers can push state updates to the TUI panel in real time:
# In your tool handler (runs in the extension subprocess):
Main.Kaimon.KaimonGate.push_panel("key", value)
# Batch form:
Main.Kaimon.KaimonGate.push_panel("greetings" => greetings, "status" => "ready")Values can be any serializable Julia type — strings, numbers, vectors, dicts, etc. They're delivered via ZMQ PUB/SUB with no blocking.
On the panel side, pushed values appear in ctx._cache[:panel_state] as a Dict{String,Any}:
function update!(state, ctx)
ps = get(ctx._cache, :panel_state, nothing)
ps === nothing && return
if haskey(ps, "greetings")
state.greetings = ps["greetings"]
end
endUse Main.Kaimon.KaimonGate
Extension modules run in their own namespace. Since Kaimon is loaded at Main scope in the extension subprocess, you must use Main.Kaimon.KaimonGate.push_panel() — not just KaimonGate.push_panel().
Copy mutable values
Always copy() mutable values before pushing: push_panel("data", copy(vec)). The value is serialized asynchronously, and the original may be mutated before serialization completes.
Shutdown Hooks
Declare shutdown_function in kaimon.toml to run cleanup logic before the extension exits:
function on_shutdown()
# Flush pending writes, close connections, save state...
@info "Extension shutting down"
endThe hook has a 5-second timeout. If it doesn't complete in time, the process is terminated. Use this for quick cleanup only — don't block on network requests or long computations.
