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
+64 -8
View File
@@ -505,7 +505,14 @@ server:tool("remote_list_hosts",
)
end
return table.concat(lines, "\n")
end
end,
{ annotations = {
title = "List fleet hosts",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = true,
} }
)
server:tool("remote_shell", "Run a shell command on a fleet host. lmcp-primary with ssh fallback.",
@@ -515,7 +522,14 @@ server:tool("remote_shell", "Run a shell command on a fleet host. lmcp-primary w
cwd = { type = "string", description = "Working directory" },
timeout = { type = "integer", description = "Timeout (seconds)", default = 120 },
}, required = { "host", "command" } },
function(a) return call_remote("shell", a, true, ssh_shell) end
function(a) return call_remote("shell", a, true, ssh_shell) end,
{ annotations = {
title = "Remote shell",
readOnlyHint = false,
destructiveHint = true,
idempotentHint = false,
openWorldHint = true,
} }
)
server:tool("remote_read_file", "Read a file from a fleet host.",
@@ -523,7 +537,14 @@ server:tool("remote_read_file", "Read a file from a fleet host.",
host = HOST_ARG,
path = { type = "string", description = "File path" },
}, required = { "host", "path" } },
function(a) return call_remote("read_file", a, true, ssh_read_file) end
function(a) return call_remote("read_file", a, true, ssh_read_file) end,
{ annotations = {
title = "Remote read file",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = true,
} }
)
server:tool("remote_write_file", "Write content to a file on a fleet host.",
@@ -532,7 +553,14 @@ server:tool("remote_write_file", "Write content to a file on a fleet host.",
path = { type = "string" },
content = { type = "string" },
}, required = { "host", "path", "content" } },
function(a) return call_remote("write_file", a, true, ssh_write_file) end
function(a) return call_remote("write_file", a, true, ssh_write_file) end,
{ annotations = {
title = "Remote write file",
readOnlyHint = false,
destructiveHint = true,
idempotentHint = true,
openWorldHint = true,
} }
)
server:tool("remote_edit_file",
@@ -544,7 +572,14 @@ server:tool("remote_edit_file",
new_string = { type = "string" },
replace_all = { type = "boolean", default = false },
}, required = { "host", "path", "old_string", "new_string" } },
function(a) return call_remote("edit_file", a, false, nil) end
function(a) return call_remote("edit_file", a, false, nil) end,
{ annotations = {
title = "Remote edit file",
readOnlyHint = false,
destructiveHint = true,
idempotentHint = false,
openWorldHint = true,
} }
)
server:tool("remote_shell_bg",
@@ -555,7 +590,14 @@ server:tool("remote_shell_bg",
cwd = { type = "string" },
log = { type = "string", description = "Log file path" },
}, required = { "host", "command" } },
function(a) return call_remote("shell_bg", a, false, nil) end
function(a) return call_remote("shell_bg", a, false, nil) end,
{ annotations = {
title = "Remote shell (background)",
readOnlyHint = false,
destructiveHint = true,
idempotentHint = false,
openWorldHint = true,
} }
)
server:tool("remote_list_dir", "List directory entries on a fleet host.",
@@ -563,7 +605,14 @@ server:tool("remote_list_dir", "List directory entries on a fleet host.",
host = HOST_ARG,
path = { type = "string", default = "." },
}, required = { "host" } },
function(a) return call_remote("list_dir", a, true, ssh_list_dir) end
function(a) return call_remote("list_dir", a, true, ssh_list_dir) end,
{ annotations = {
title = "Remote list directory",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = true,
} }
)
server:tool("remote_search_files", "find-by-pattern on a fleet host.",
@@ -572,7 +621,14 @@ server:tool("remote_search_files", "find-by-pattern on a fleet host.",
pattern = { type = "string" },
path = { type = "string", default = "/" },
}, required = { "host", "pattern" } },
function(a) return call_remote("search_files", a, true, ssh_search_files) end
function(a) return call_remote("search_files", a, true, ssh_search_files) end,
{ annotations = {
title = "Remote find files",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = true,
} }
)
io.stderr:write(string.format("lmcp-hub starting on port %d with %d backends from %s\n",