Initial release: Lua MCP server library

Zero-dependency MCP (Model Context Protocol) server in pure Lua.
Only requires luasocket. 2MB RSS vs Python FastMCP's 97MB.

- json.lua: pure Lua JSON encoder/decoder (~150 lines)
- lmcp.lua: MCP server with streamable-http transport (~230 lines)
- example_server.lua: shell/file tools demo

Implements MCP 2025-03-26: initialize, tools/list, tools/call,
notifications/initialized, ping. JSON-RPC 2.0. SSE support. CORS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 15:54:25 +00:00
commit 2bd661a8c9
3 changed files with 600 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env lua
-- Example lmcp server — shell tools
-- Usage: lua example_server.lua [port]
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,
})
server:tool("shell", "Execute a shell command", {
type = "object",
properties = {
command = { type = "string", description = "Shell command to execute" },
timeout = { type = "integer", description = "Timeout in seconds", default = 30 },
},
required = { "command" },
}, 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", description = "File path to read" },
},
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()