AI Agents & MCP
Keybind runs a local MCP server (off by default, loopback-only, bearer-token authenticated — see Getting Started for connecting a client). Once a client like Claude is connected, a module can go further than the built-in read/control tools: it can expose its own functions as agent-callable tools, and push its own events back to the agent. Both directions are off by default and layered with consent, so nothing here changes what an already-installed module can do until the module's author *and* you both opt in.
The master switch
Everything on this page sits behind one setting: Settings → MCP → "Allow connected AI agents to call module tools" (config key mcp.exposeModuleTools, default off). While it's off:
- No module's exports are advertised to a connected agent, no matter what the module's manifest declares.
- The event-drain tools (below) return no events, even if a module keeps pushing them.
Turning it on doesn't bypass per-call consent — it just allows the *possibility* of a module being reachable at all. Think of it as the breaker for this whole feature; the per-call approval dialog is the switch on each individual light.
Direction 1: an agent calls your module's functions
A module opts a function in by listing its name in a new agent_tools array in ksl.json:
{
"name": "app-watcher",
"entry": "main.ksl",
"capabilities": ["process", "notify"],
"agent_tools": ["restart_target", "get_status"]
}
Only export fns actually named here are reachable — anything not listed stays completely private, even if the module exports many more functions. Each listed export becomes its own MCP tool, named {module_id}__{functionName} (e.g. app-watcher__restart_target), with an input schema built from that export's parameter names.
Calling one of these tools is CONTROL-tier: exactly like run_keybind or run_script, the user is asked to approve the specific call (or set an "always allow"/"always deny" for that tool going forward). An export named in agent_tools is ordinary KSL code from the module's point of view — invoking it from an agent is exactly as consequential as running any other script, so it gets the same consent treatment.
The Modules tab shows an "Agent tools" badge on any module that declares agent_tools, so you can see at a glance which installed modules have opted functions in — independent of whether the master switch above is currently on.
Direction 2: your module pushes events to the agent
The reverse channel: a module can proactively notify a connected agent that something happened, instead of waiting to be called. This needs the new `agent` capability (non-safe — declare it explicitly, like network or ipc):
#! requires: agent
fn on_scan_complete(count) {
agent_emit("scan-complete", { "found": count })
}agent_emit(event, payload?) takes an event name and an optional JSON-serializable payload, and returns true once the event is enqueued. It pushes onto a single process-wide queue (not per-module) — a connected agent drains it with its own MCP tools, not a KSL builtin:
- `poll_agent_events(since?)` — returns everything queued after cursor
since(omit or pass0for everything currently queued), shaped as{events: [{id, ts_ms, module_id, event, payload}], cursor}. It never blocks. - `wait_agent_event(timeout_ms?, since?)` — the long-poll version: blocks until at least one new event arrives or
timeout_mselapses (default 25000ms, max 60000ms), then returns the same shape. Prefer this over callingpoll_agent_eventsin a tight loop.
Both tools return the queue's current high-water id as cursor — pass that back as since on your next call to only see newer events. Like Direction 1, both drain tools return an empty {events: [], cursor: 0} immediately whenever the master switch is off, so a module happily calling agent_emit has no effect at all until the user has turned the feature on.
Safety summary
- Off by default at every layer: the MCP server itself is loopback/bearer-auth and off until you start it; the modules↔agent bridge is off until you flip
mcp.exposeModuleTools; a module exposes nothing until its author opts specific functions intoagent_tools; pushing events needs the explicitagentcapability grant. - Per-module, per-function opt-in —
agent_toolsis an allow-list, not a default. An export left off the list is never reachable by an agent, full stop. - Consent on every inbound call — an agent calling one of your module's tools goes through the same CONTROL-tier approval dialog as
run_keybind/run_script, one decision per tool (with an "always allow" option if you trust it). - The reverse channel is read-only for the agent — draining events has no side effects on your machine; the module chose to emit them.