initial import: lmcp 0.1.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 19:58:40 +00:00
parent 6bf0f450dc
commit c6efc8f685
5 changed files with 335 additions and 217 deletions
+57 -84
View File
@@ -1,100 +1,73 @@
#!/usr/bin/env lua
-- Example lmcp server — customize tools for your host
local dir = arg[0]:match("(.*/)") or "./"
package.path = package.path .. ";" .. dir .. "?.lua"
local lmcp = require("lmcp")
-- Example lmcp server — shell tools
-- Usage: lua example_server.lua [port]
local server = lmcp.new("my-tools", {
port = tonumber(os.getenv("LMCP_PORT")) or 8080,
-- Optional: Bearer token auth from config file
-- conf = dir .. "lmcp.conf",
-- Or set directly:
-- auth_token = "my-secret-token",
local dir = arg[0]:match('(.*/)') or './'
package.path = package.path .. ';' .. dir .. '?.lua'
local lmcp = require('lmcp')
local server = lmcp.new("example-tools", {
port = tonumber(arg[1]) or 8080,
})
-- Non-blocking shell execution with timeout
local function run(cmd, timeout_sec)
timeout_sec = timeout_sec or 120
local base = os.tmpname()
local out_file = base .. ".out"
local done_file = base .. ".done"
local sh_cmd = string.format("(%s) > '%s' 2>&1; echo $? > '%s'", cmd, out_file, done_file)
os.execute("sh -c ' " .. sh_cmd .. " ' &")
local elapsed = 0
local interval = 50
while elapsed < timeout_sec * 1000 do
local f = io.open(done_file, "r")
if f then f:close(); break end
if interval < 1000 then
os.execute("sleep 0." .. string.format("%03d", interval))
else
os.execute("sleep " .. math.ceil(interval / 1000))
end
elapsed = elapsed + interval
if interval < 2000 then interval = math.floor(interval * 1.5) end
end
local f = io.open(out_file, "r")
local output = f and f:read("*a") or ""
if f then f:close() end
os.remove(out_file); os.remove(done_file); os.remove(base)
if elapsed >= timeout_sec * 1000 then return "Error: timed out after " .. timeout_sec .. "s" end
return output ~= "" and output or "(no output)"
end
-- Register tools
server:tool("shell", "Execute a shell command.", {
server:tool("shell", "Execute a shell command", {
type = "object",
properties = {
command = { type = "string" },
cwd = { type = "string" },
timeout = { type = "integer", default = 120 },
command = { type = "string", description = "Shell command to execute" },
timeout = { type = "integer", description = "Timeout in seconds", default = 30 },
},
required = { "command" },
}, function(a)
local c = a.command
if a.cwd then c = "cd \"" .. a.cwd .. "\" && " .. c end
return run(c, a.timeout or 120)
}, function(args)
local handle = io.popen(args.command .. ' 2>&1', 'r')
if not handle then return "Error: could not execute command" end
local result = handle:read('*a')
handle:close()
return result ~= '' and result or '(no output)'
end)
server:tool("read_file", "Read a file.", {
type = "object",
properties = { path = { type = "string" } },
required = { "path" },
}, function(a)
local f = io.open(a.path, "r")
if not f then return "Error: could not read " .. a.path end
local c = f:read("*a"); f:close(); return c
end)
server:tool("write_file", "Write content to a file.", {
type = "object",
properties = { path = { type = "string" }, content = { type = "string" } },
required = { "path", "content" },
}, function(a)
local f = io.open(a.path, "w")
if not f then return "Error: could not write " .. a.path end
f:write(a.content); f:close()
return string.format("Written %d bytes to %s", #a.content, a.path)
end)
server:tool("list_dir", "List directory contents.", {
type = "object",
properties = { path = { type = "string", default = "." } },
}, function(a)
return run("ls -la '" .. (a.path or ".") .. "'", 10)
end)
server:tool("search_files", "Search for files by name.", {
server:tool("read_file", "Read a file", {
type = "object",
properties = {
pattern = { type = "string" },
path = { type = "string", default = "/" },
max_depth = { type = "integer", default = 4 },
path = { type = "string", description = "File path to read" },
},
required = { "pattern" },
}, function(a)
return run(string.format("find '%s' -maxdepth %d -name '%s' 2>/dev/null",
a.path or "/", a.max_depth or 4, a.pattern), 30)
required = { "path" },
}, function(args)
local f = io.open(args.path, 'r')
if not f then return "Error: could not open " .. args.path end
local content = f:read('*a')
f:close()
return content
end)
server:tool("write_file", "Write content to a file", {
type = "object",
properties = {
path = { type = "string", description = "File path to write" },
content = { type = "string", description = "Content to write" },
},
required = { "path", "content" },
}, function(args)
local f = io.open(args.path, 'w')
if not f then return "Error: could not open " .. args.path .. " for writing" end
f:write(args.content)
f:close()
return string.format("Written %d bytes to %s", #args.content, args.path)
end)
server:tool("list_dir", "List directory contents", {
type = "object",
properties = {
path = { type = "string", description = "Directory path", default = "." },
},
}, function(args)
local path = args.path or '.'
local handle = io.popen('ls -1 ' .. path:gsub("'", "'\\''") .. ' 2>&1')
if not handle then return "Error: could not list " .. path end
local result = handle:read('*a')
handle:close()
return result
end)
io.stderr:write("Starting lmcp example server...\n")
server:run()