Skip to content

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:

  1. Defines handler functions for one or more tools.

  2. Declares those tools in a kaimon.toml manifest at the project root.

  3. 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:

toml
[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"
FieldRequiredDescription
namespaceYesDot-prefix for all tool names (e.g., myext.tool_name)
moduleYesJulia module name to using
tools_functionYesExported function that returns Vector{GateTool}
descriptionNoHuman-readable summary for display in the TUI and extension_info
shutdown_functionNoExported no-arg function called before the extension process exits (5 s timeout)
tui_fileNoPath to a lightweight TUI panel file (relative to project root). Press [u] on the extension in the Extensions tab to open it.
julia_flagsNoJulia startup flags for the extension process (e.g., ["-t4,1", "--heap-size-hint=1G"]). Defaults to -t auto.
event_topicsNoStream 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]NoEnvironment 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:

json
{
  "extensions": [
    {
      "project_path": "/path/to/MyExtension.jl",
      "enabled": true,
      "auto_start": true
    }
  ]
}
FieldDescription
project_pathAbsolute path to the extension project directory
enabledWhether the extension can be started
auto_startIf 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:

StateDescription
:stoppedNot running
:startingSubprocess spawned, waiting for Gate connection
:runningConnected and serving tools
:crashedProcess 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:

KeyDefaultMeaning
extension_tool_hold_seconds45How long the tools stay listed while the extension is down. Raise it if an extension's precompile is slow.
extension_tool_park_seconds40How 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:

  1. Gate receives shutdown signal.

  2. If shutdown_function is declared, the hook is called with a 5-second timeout. Use this to flush state, close connections, or log a shutdown message.

  3. If the hook completes or times out, the process exits normally.

  4. If the process does not exit, Kaimon sends SIGTERM, then SIGKILL after 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 as smlabnotes.search

  • If 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 ​

KeyAction
aAdd a new extension (enter project path)
dRemove selected extension
eToggle enabled/disabled
tToggle auto-start
sStart extension (if stopped)
xStop extension (if running)
rRestart extension
uOpen TUI panel (if tui_file defined)
EnterExpand detail view
EscClose 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 schemas

The 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 panel

See examples/HelloExtension.jl for a complete working example with tools, push-based panel updates, and a shutdown hook.

kaimon.toml ​

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"          # optional

Defining 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.

julia
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

end

Registering ​

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:

julia
# 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:

FunctionRequiredSignatureDescription
initYes(ctx) → stateCreate initial panel state
update!No(state, ctx)Called each frame (~60 fps); read pushed data here
viewYes(state, area::Rect, buf::Buffer)Render into a Tachikoma buffer region
handle_key!No(state, evt::KeyEvent) → BoolProcess input; return true if consumed
cleanup!Yes(state, ctx)Tear down when panel closes

Lifecycle ​

  1. User presses u on a running extension in the Extensions tab

  2. Kaimon include()s the tui_file into a fresh anonymous module

  3. init(ctx) is called to create the panel state

  4. Each frame: update!(state, ctx) then view(state, area, buf)

  5. Key events route to handle_key!(state, evt) — return true to consume

  6. When the user presses Esc, cleanup!(state, ctx) is called and the panel closes

ExtPanelContext ​

The ctx argument passed to init, update!, and cleanup! provides:

FieldTypeDescription
session_keyString8-char gate session key for this extension
tickIntFrame counter (increments each frame)
_cacheDict{Symbol,Any}Scratch space; :panel_state is auto-populated by push_panel
evalFunctioneval(code::String) → NamedTuple — evaluate code in the extension process
requestFunctionrequest(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:

julia
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
end

KaimonGate.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:

julia
# 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}:

julia
function update!(state, ctx)
    ps = get(ctx._cache, :panel_state, nothing)
    ps === nothing && return
    if haskey(ps, "greetings")
        state.greetings = ps["greetings"]
    end
end

Use 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:

julia
function on_shutdown()
    # Flush pending writes, close connections, save state...
    @info "Extension shutting down"
end

The 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.