From 9707f7ae93f95e4f84a0ba5ec2f47626c3927a37 Mon Sep 17 00:00:00 2001 From: Markus Fritsche Date: Sun, 17 May 2026 22:39:56 +0000 Subject: [PATCH] v1.1.1: omit empty inputSchema.properties at registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same json.lua empty-table → [] gotcha that bit `ping` in v1.0.0-rc1 (project_json_empty_table_gotcha memory) bit again — this time on tool inputSchemas with `properties = {}`. Symptom: spec-strict MCP clients (Zod et al.) reject tools/list with: expected: record, code: invalid_type, path: [tools, N, inputSchema, properties], message: "Invalid input: expected record, received array" Fix: in `lmcp:tool()`, normalise the registered inputSchema — when `properties` is an empty Lua table, drop the key entirely. JSON Schema permits omitting `properties` on `type: "object"` (means "any object, no constraints" — exactly what a no-arg tool wants). Clone-before-mutate so the caller's table isn't trampled (matters when a server author shares one schema across multiple registrations). Smoke tested locally with 3 tools (empty, default-nil, populated): - `properties = {}` → emitted as `{"type":"object"}` - nil schema → same default, same output - populated properties → emitted intact with full shape Discovered against hertz-tools live (lxc_list, network_status had `properties = {}` — hertz hotfixed by hand before this commit; this protects every future tool author from the same trap). Co-Authored-By: Claude Opus 4.7 (1M context) --- lmcp.lua | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lmcp.lua b/lmcp.lua index 2a8ac73..772687c 100644 --- a/lmcp.lua +++ b/lmcp.lua @@ -163,10 +163,29 @@ end -- structuredContent (issue #13; spec-strict clients get first-class -- structured access) function lmcp:tool(name, description, params_schema, handler, opts) + -- Normalise empty inputSchema.properties → nil. JSON Schema allows + -- omitting `properties` on a `type: "object"` schema (means "any + -- object, no constraints"). Without this, an empty Lua properties + -- table goes through json.lua's is_array → emitted as `[]` → + -- spec-strict clients (Zod et al.) reject with + -- `expected: record, received: array`. The same gotcha already + -- bit `ping` in v1.0.0-rc1 (project_json_empty_table_gotcha + -- memory). v1.1.1 fix. + local schema = params_schema or { type = "object" } + if type(schema.properties) == "table" and next(schema.properties) == nil then + -- Clone the schema and drop the empty `properties` key. Avoids + -- mutating the caller's table (in case they re-use it across + -- registrations). + local clean = {} + for k, v in pairs(schema) do + if k ~= "properties" then clean[k] = v end + end + schema = clean + end self.tools[name] = { name = name, description = description, - inputSchema = params_schema or { type = "object", properties = {} }, + inputSchema = schema, handler = handler, annotations = opts and opts.annotations or nil, outputSchema = opts and opts.outputSchema or nil,