diff --git a/server.lua b/server.lua index 531b2e7..f2457f1 100644 --- a/server.lua +++ b/server.lua @@ -1066,6 +1066,46 @@ if WINDOWS then }) end +-- ---- host-local tool plugins (issue #22) ---- +-- Load every .lua file in LMCP_TOOLS_DIR (default /opt/lmcp/tools.d on POSIX, +-- %ProgramData%\lmcp\tools.d on Windows). Each file is invoked as a function +-- receiving the configured `server` instance and the `run` helper: +-- +-- local server, run = ... +-- server:tool("my_local_tool", "...", {...}, function(a) return run(...) end) +-- +-- This is the standard plugin pattern (nginx conf.d/, systemd-tmpfiles.d, …). +-- Hosts can ship their own tools alongside the packaged generics without +-- forking the upstream server.lua. +local plugin_dir = os.getenv("LMCP_TOOLS_DIR") + or (WINDOWS and (os.getenv("ProgramData") or "C:\\ProgramData") .. "\\lmcp\\tools.d" + or "/opt/lmcp/tools.d") +local list_cmd = WINDOWS + and ('dir /b "' .. plugin_dir .. '\\*.lua" 2>nul') + or ('ls -1 "' .. plugin_dir .. '"/*.lua 2>/dev/null') +local lh = io.popen(list_cmd) +if lh then + for path in lh:lines() do + -- On Windows `dir /b` emits bare filenames; prefix the dir. + local full = path:match("[/\\]") and path + or (plugin_dir .. (WINDOWS and "\\" or "/") .. path) + local chunk, err = loadfile(full) + if chunk then + local ok, perr = pcall(chunk, server, run) + if ok then + io.stderr:write("lmcp: loaded plugin " .. full .. "\n") + else + io.stderr:write("lmcp: plugin " .. full .. " errored: " + .. tostring(perr) .. "\n") + end + else + io.stderr:write("lmcp: plugin " .. full .. " load error: " + .. tostring(err) .. "\n") + end + end + lh:close() +end + local transport = os.getenv("LMCP_TRANSPORT") or "http" if transport == "stdio" then if os.getenv("LMCP_PORT") then