initial import: lmcp 0.1.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,20 +21,8 @@ local function read_conf(path)
|
||||
end
|
||||
|
||||
-- Protocol constants
|
||||
-- Constant-time string comparison (prevents timing oracle on auth token)
|
||||
local function constant_time_eq(a, b)
|
||||
if type(a) ~= "string" or type(b) ~= "string" then return false end
|
||||
if #a ~= #b then return false end -- length leak is acceptable (token length is not secret)
|
||||
local diff = 0
|
||||
for i = 1, #a do
|
||||
diff = diff + (a:byte(i) ~ b:byte(i)) -- Lua 5.4 bitwise XOR
|
||||
end
|
||||
return diff == 0
|
||||
end
|
||||
|
||||
local MCP_VERSION = "2025-03-26"
|
||||
local JSONRPC = "2.0"
|
||||
local MAX_BODY_SIZE = 65536 -- 64KB, generous for MCP JSON-RPC
|
||||
|
||||
function lmcp.new(name, opts)
|
||||
opts = opts or {}
|
||||
@@ -46,9 +34,7 @@ function lmcp.new(name, opts)
|
||||
self.tools = {}
|
||||
self._session_id = nil
|
||||
-- Auth: explicit opt > conf file > nil (no auth)
|
||||
if os.getenv("LMCP_TOKEN") then
|
||||
self._auth_token = os.getenv("LMCP_TOKEN")
|
||||
elseif opts.auth_token then
|
||||
if opts.auth_token then
|
||||
self._auth_token = opts.auth_token
|
||||
elseif opts.conf then
|
||||
local conf = read_conf(opts.conf)
|
||||
@@ -145,8 +131,6 @@ function lmcp:handle_request(req)
|
||||
end
|
||||
|
||||
else
|
||||
-- Unknown notifications must be silently ignored (JSON-RPC spec)
|
||||
if id == nil then return nil end
|
||||
return jsonrpc_error(id, -32601, "Method not found: " .. tostring(method))
|
||||
end
|
||||
end
|
||||
@@ -173,9 +157,6 @@ local function parse_http_request(client)
|
||||
-- Read body
|
||||
local body = ''
|
||||
local content_length = tonumber(headers['content-length'] or 0)
|
||||
if content_length > MAX_BODY_SIZE then
|
||||
return nil, "body too large"
|
||||
end
|
||||
if content_length > 0 then
|
||||
body, err = client:receive(content_length)
|
||||
if not body then return nil, err end
|
||||
@@ -221,7 +202,7 @@ function lmcp:serve_request(client)
|
||||
if self._auth_token and req.method ~= 'OPTIONS' then
|
||||
local auth = req.headers['authorization'] or ''
|
||||
local token = auth:match('^Bearer%s+(.+)$')
|
||||
if not constant_time_eq(token, self._auth_token) then
|
||||
if token ~= self._auth_token then
|
||||
send_response(client, '401 Unauthorized',
|
||||
{ ['Content-Type'] = 'application/json',
|
||||
['WWW-Authenticate'] = 'Bearer' },
|
||||
@@ -232,7 +213,7 @@ function lmcp:serve_request(client)
|
||||
end
|
||||
|
||||
-- GET /mcp — SSE endpoint (for session establishment)
|
||||
if req.method == 'GET' and (path:match('^/mcp$') or path:match('^/mcp%?')) then
|
||||
if req.method == 'GET' and path:match('^/mcp') then
|
||||
-- SSE stream — send headers and keep alive briefly
|
||||
local sse_headers = {
|
||||
'HTTP/1.1 200 OK',
|
||||
@@ -259,7 +240,7 @@ function lmcp:serve_request(client)
|
||||
end
|
||||
|
||||
-- POST /mcp — JSON-RPC endpoint
|
||||
if req.method == 'POST' and (path:match('^/mcp$') or path:match('^/mcp%?')) then
|
||||
if req.method == 'POST' and path:match('^/mcp') then
|
||||
if req.body == '' then
|
||||
send_response(client, '400 Bad Request',
|
||||
{ ['Content-Type'] = 'application/json' },
|
||||
@@ -309,12 +290,19 @@ function lmcp:serve_request(client)
|
||||
return
|
||||
end
|
||||
|
||||
-- OPTIONS (CORS preflight)
|
||||
-- OPTIONS (CORS preflight). Echo back whatever the client asked for
|
||||
-- (MCP adds e.g. Mcp-Session-Id, Mcp-Protocol-Version); fall back to *.
|
||||
if req.method == 'OPTIONS' then
|
||||
local acrh = req.headers and (req.headers['access-control-request-headers']
|
||||
or req.headers['Access-Control-Request-Headers'])
|
||||
send_response(client, '204 No Content', {
|
||||
['Access-Control-Allow-Origin'] = '*',
|
||||
['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS',
|
||||
['Access-Control-Allow-Headers'] = 'Content-Type, Accept, Authorization',
|
||||
-- CORS spec: '*' does NOT cover Authorization; must list it explicitly.
|
||||
-- Echo back whatever the client requested plus Authorization.
|
||||
['Access-Control-Allow-Headers'] = acrh and (acrh .. ', Authorization')
|
||||
or 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version',
|
||||
['Access-Control-Max-Age'] = '86400',
|
||||
}, '')
|
||||
client:close()
|
||||
return
|
||||
@@ -360,4 +348,3 @@ function lmcp:run()
|
||||
end
|
||||
|
||||
return lmcp
|
||||
|
||||
|
||||
Reference in New Issue
Block a user