v1.0.0-rc1: full MCP 2025-06-18 surface

Closes 14 issues; lmcp now implements the complete client-facing
surface of MCP spec 2025-06-18.

New primitives:
  - fetch (#3)              HTTP GET/HEAD with bounded body + render chain
  - web_search (#4)         pluggable backend (SearXNG/DDG/Tavily/Brave)
  - Resources (#5)          resources/list, /read, /templates/list + list_changed
  - Prompts (#6)            prompts/list, /get + list_changed
  - Completion (#7)         completion/complete for prompt/template args
  - Logging (#8)            logging/setLevel + notifications/message
  - Sampling (#9)           server-initiated sampling/createMessage
  - Roots (#10)             roots/list + cache + path_in_roots helper

Protocol / wire:
  - Pagination (#12)        cursor on tools|resources|prompts/list
  - Structured tool output (#13)  structuredContent + _meta + protoV bump to 2025-06-18
  - Tool annotations (#14)  readOnlyHint/destructive/idempotent/openWorld on all tools
  - stdio transport (#15)   LMCP_TRANSPORT=stdio for Claude Desktop / IDE clients
  - Streamable HTTP (#16)   select()-based event loop, sessions, persistent SSE,
                            DELETE, heartbeat, server-initiated request helper
  - ping (#19)              now emits result:{} not result:[] via json.empty_object

Cross-cutting fixes:
  - json.lua: UTF-16 surrogate pair combination (emoji/non-BMP CJK round-trip)
  - json.lua: json.empty_object sentinel for spec-correct {} emission
  - handle_request: generic notification suppression (id==nil → return nil)
    eliminates malformed -32601 with id:null on stdio and HTTP transports

Tool annotations backfilled across all registrations:
  - server.lua: 10 tools (shell, shell_bg, read_file, write_file, edit_file,
    list_dir, search_files, fetch, web_search, systeminfo)
  - hub.lua:   8 remote_* tools
  - example_server.lua: 4 demo tools + 3 sample resources + 1 sample prompt
                        + 1 sample completer

Honest limits, filed as follow-up issues:
  - #11 progress + cancellation — gated on #20 (handler concurrency)
  - #18 windows/pkg sync         — stale April-2026 snapshot, packaging decision
  - #20 concurrent handler dispatch — select() loop concurrencies I/O, not
                                      handler execution; synchronous tool
                                      handlers still serialise (shell sleep 3
                                      blocks a parallel ping)

Backwards compatible: every previously-deployed lmcp client (sessionless
POST, HTTP-only, no Mcp-Session-Id awareness) keeps working unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 17:15:54 +00:00
parent b81b021b5b
commit deb73d129e
5 changed files with 2135 additions and 178 deletions
+26 -2
View File
@@ -60,6 +60,13 @@ encode_value = function(v)
local t = type(v)
if v == nil or v == json.null then
return 'null'
elseif v == json.empty_object then
-- Sentinel for forcing {} (object) instead of [] (array) when
-- the field semantically requires an object but is empty.
-- Without this, every empty Lua table goes through is_array()
-- and emits as [], breaking spec-strict JSON-RPC consumers
-- (e.g. ping result, MUST be {}).
return '{}'
elseif t == 'boolean' then
return v and 'true' or 'false'
elseif t == 'number' then
@@ -110,9 +117,20 @@ local function decode_string(s, pos)
pos = pos + 1
c = s:sub(pos, pos)
if c == 'u' then
local hex = s:sub(pos + 1, pos + 4)
parts[#parts + 1] = utf8.char(tonumber(hex, 16))
local cp = tonumber(s:sub(pos + 1, pos + 4), 16)
pos = pos + 5
-- Combine UTF-16 surrogate pair so non-BMP chars (emoji,
-- supplementary CJK) decode correctly instead of as two
-- lone surrogates → invalid UTF-8.
if cp and cp >= 0xD800 and cp <= 0xDBFF
and s:sub(pos, pos + 1) == "\\u" then
local lo = tonumber(s:sub(pos + 2, pos + 5), 16)
if lo and lo >= 0xDC00 and lo <= 0xDFFF then
cp = (cp - 0xD800) * 0x400 + (lo - 0xDC00) + 0x10000
pos = pos + 6
end
end
parts[#parts + 1] = utf8.char(cp)
else
local esc = { n = '\n', r = '\r', t = '\t', b = '\b', f = '\f' }
parts[#parts + 1] = esc[c] or c
@@ -210,6 +228,12 @@ end
-- Sentinel for JSON null
json.null = setmetatable({}, { __tostring = function() return 'null' end })
-- Sentinel for an empty JSON object ({}). Use when a field semantically
-- requires an object but is empty — e.g. `ping` result, MCP _meta = {}.
-- Without this, an empty Lua table goes through is_array() → '[]'.
-- See memory project_json_empty_table_gotcha.md.
json.empty_object = setmetatable({}, { __tostring = function() return '{}' end })
-- Helper: encode a table as a JSON array even if empty
function json.array(t)
return setmetatable(t or {}, { __is_array = true })