ping emits result:[] not result:{} (json.lua empty-table gotcha) #19

Closed
opened 2026-05-17 16:27:07 +00:00 by claude-noether · 1 comment
Collaborator

ping currently emits {"result":[],"id":1,"jsonrpc":"2.0"}result is [] (empty array) but the MCP spec says ping returns result: {} (empty object). Spec-strict clients may reject the response.

Root cause

jsonrpc_result(id, {}) at lmcp.lua:97 passes an empty Lua table to json.encode. json.lua's is_array returns true for any table where next(t) == nil and emits []. This is the project-memorialised gotcha (project_json_empty_table_gotcha.md) — present since v0.1.0, latent on HTTP transport, surfaced during the stdio work (issue #15) where every byte goes through a single pipe.

Fix

Two options at lmcp.lua:97:

  1. Omit result entirely (cleanest): JSON-RPC 2.0 allows the absence of result only for errors, so this is technically wrong. NOT recommended.
  2. Use a sentinel for empty objects. Add json.empty_object = setmetatable({}, {__jsontype = "object"}) (or similar) in json.lua; teach json.encode to honour the metatable; use json.empty_object at the call site. Cleanest, also fixes any future case where an empty object must be emitted.
  3. Inline JSON literal. Return the raw string {jsonrpc:2.0,id: .. id .. ,result:{}} from a small ping-specific helper. Surgical but loses uniformity.

Recommendation

Option 2 — add json.empty_object sentinel. Future-proofs any field that must serialise as {}. The pattern would also benefit notifications/*.params if we ever need to emit it (currently omitted per the same memory entry).

Scope

  • json.lua: add empty_object sentinel + encoder branch (5-10 lines).
  • lmcp.lua:97: change jsonrpc_result(id, {})jsonrpc_result(id, json.empty_object).
  • Audit other empty-table emission sites: search for jsonrpc_result(id, {}) and params = {} — none currently active besides ping.

Priority

Low — has been broken since v0.1.0 with no client complaints. Worth fixing as defensive correctness, especially before issue #16 (Streamable HTTP) lands which may attract spec-strict clients. Surfaced during issue #15 Phase 7 verification.

`ping` currently emits `{"result":[],"id":1,"jsonrpc":"2.0"}` — `result` is `[]` (empty array) but the MCP spec says `ping` returns `result: {}` (empty object). Spec-strict clients may reject the response. ## Root cause `jsonrpc_result(id, {})` at `lmcp.lua:97` passes an empty Lua table to `json.encode`. `json.lua`'s `is_array` returns true for any table where `next(t) == nil` and emits `[]`. This is the project-memorialised gotcha (`project_json_empty_table_gotcha.md`) — present since v0.1.0, latent on HTTP transport, surfaced during the stdio work (issue #15) where every byte goes through a single pipe. ## Fix Two options at `lmcp.lua:97`: 1. **Omit `result` entirely** (cleanest): JSON-RPC 2.0 allows the absence of `result` only for errors, so this is technically wrong. NOT recommended. 2. **Use a sentinel for empty objects.** Add `json.empty_object = setmetatable({}, {__jsontype = "object"})` (or similar) in `json.lua`; teach `json.encode` to honour the metatable; use `json.empty_object` at the call site. Cleanest, also fixes any future case where an empty object must be emitted. 3. **Inline JSON literal.** Return the raw string `{jsonrpc:2.0,id: .. id .. ,result:{}}` from a small `ping`-specific helper. Surgical but loses uniformity. ## Recommendation Option 2 — add `json.empty_object` sentinel. Future-proofs any field that must serialise as `{}`. The pattern would also benefit `notifications/*.params` if we ever need to emit it (currently omitted per the same memory entry). ## Scope - `json.lua`: add `empty_object` sentinel + encoder branch (5-10 lines). - `lmcp.lua:97`: change `jsonrpc_result(id, {})` → `jsonrpc_result(id, json.empty_object)`. - Audit other empty-table emission sites: search for `jsonrpc_result(id, {})` and `params = {}` — none currently active besides ping. ## Priority **Low** — has been broken since v0.1.0 with no client complaints. Worth fixing as defensive correctness, especially before issue #16 (Streamable HTTP) lands which may attract spec-strict clients. Surfaced during issue #15 Phase 7 verification.
Author
Collaborator

Implemented in this session — added json.empty_object sentinel (Option 2 from the issue body) and switched ping to use it. Verified live over stdio: ping now emits {"jsonrpc":"2.0","result":{},"id":1} (was result:[]).

Other empty-array emissions (resources/list, tools/list, prompts/list etc.) correctly stay as [] per spec — those are arrays, not objects.

Implemented in this session — added `json.empty_object` sentinel (Option 2 from the issue body) and switched `ping` to use it. Verified live over stdio: `ping` now emits `{"jsonrpc":"2.0","result":{},"id":1}` (was `result:[]`). Other empty-array emissions (`resources/list`, `tools/list`, `prompts/list` etc.) correctly stay as `[]` per spec — those are arrays, not objects.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marfrit/lmcp#19