Add structuredContent + _meta passthrough (2025-06-18) #13

Closed
opened 2026-05-17 15:56:09 +00:00 by claude-noether · 1 comment
Collaborator

Add structured tool output (structuredContent) and _meta field passthrough — both formalised in the 2025-06-18 spec revision.

Goal

Today lmcp tools return strings (single text block) or arbitrary Lua tables (auto-JSON-encoded into a single text block per lmcp.lua:117–135). The fetch and web_search tools both return JSON-encoded tables embedded inside text — semantically lossy, since the client has to parse the text back to JSON to use the structure. The spec's structuredContent returns the structured payload as a first-class field alongside the text block.

_meta is a passthrough free-form object on every request/response/notification. Clients use it for trace IDs, progress tokens, custom annotations. lmcp ignores it today; the spec requires servers preserve and echo it where applicable.

Changes

Tool response shape

Today (lmcp tools/call result):

{ content: [{ type: "text", text: "<json>" }], isError: false }

Spec-compliant (when handler returns a table):

{
  content: [{ type: "text", text: "<json>" }],        -- backwards-compat
  structuredContent: { … },                            -- the structured form
  isError: false,
}

Handler convention

server:tool("fetch", "…", schema, function(args)
    return {
        structured = { ok = true, status = 200, body = "..." },
        text = "Fetched 528 bytes (HTTP 200)",        -- optional human summary
    }
end)

If structured is set, lmcp emits structuredContent. If both structured and text are set, both are returned. If neither (plain string), behaviour matches today.

outputSchema declaration

Tools may declare outputSchema (JSON Schema) at registration; clients can validate the structured content against it.

_meta passthrough

  • On tools/call: read params._meta, hand it to the handler context.
  • On responses and notifications: allow the handler to set _meta on its return.
  • On notifications/progress etc.: echo the request's _meta.progressToken.

Capabilities

No new capability flag — these are protocol-version-tagged features. Bump protocolVersion to "2025-06-18" (see separate issue).

Scope (v1)

  • New handler-return convention { structured = …, text = …, isError = … }.
  • Echo structuredContent in tool responses.
  • Optional outputSchema field on tool registration.
  • _meta read/passthrough on tools/call and the future progress/cancelled notifications.

Out of scope

  • Server-side JSON Schema validation of structured output against declared outputSchema (the spec puts that on the client).

Priority

Medium. fetch and web_search would benefit immediately; without this, every JSON-shaped tool result is wrapped-then-reparsed by the client. Half a day after the Protocol Version bump issue.

Add **structured tool output** (`structuredContent`) and `_meta` field passthrough — both formalised in the `2025-06-18` spec revision. ## Goal Today lmcp tools return strings (single text block) or arbitrary Lua tables (auto-JSON-encoded into a single text block per `lmcp.lua:117–135`). The `fetch` and `web_search` tools both return JSON-encoded tables embedded inside text — semantically lossy, since the client has to parse the text back to JSON to use the structure. The spec's `structuredContent` returns the structured payload as a first-class field alongside the text block. `_meta` is a passthrough free-form object on every request/response/notification. Clients use it for trace IDs, progress tokens, custom annotations. lmcp ignores it today; the spec requires servers preserve and echo it where applicable. ## Changes ### Tool response shape Today (lmcp `tools/call` result): ``` { content: [{ type: "text", text: "<json>" }], isError: false } ``` Spec-compliant (when handler returns a table): ``` { content: [{ type: "text", text: "<json>" }], -- backwards-compat structuredContent: { … }, -- the structured form isError: false, } ``` ### Handler convention ```lua server:tool("fetch", "…", schema, function(args) return { structured = { ok = true, status = 200, body = "..." }, text = "Fetched 528 bytes (HTTP 200)", -- optional human summary } end) ``` If `structured` is set, lmcp emits `structuredContent`. If both `structured` and `text` are set, both are returned. If neither (plain string), behaviour matches today. ### `outputSchema` declaration Tools may declare `outputSchema` (JSON Schema) at registration; clients can validate the structured content against it. ### `_meta` passthrough - On `tools/call`: read `params._meta`, hand it to the handler context. - On responses and notifications: allow the handler to set `_meta` on its return. - On `notifications/progress` etc.: echo the request's `_meta.progressToken`. ## Capabilities No new capability flag — these are protocol-version-tagged features. Bump `protocolVersion` to `"2025-06-18"` (see separate issue). ## Scope (v1) - New handler-return convention `{ structured = …, text = …, isError = … }`. - Echo `structuredContent` in tool responses. - Optional `outputSchema` field on tool registration. - `_meta` read/passthrough on `tools/call` and the future `progress`/`cancelled` notifications. ## Out of scope - Server-side JSON Schema validation of structured output against declared `outputSchema` (the spec puts that on the client). ## Priority **Medium**. `fetch` and `web_search` would benefit immediately; without this, every JSON-shaped tool result is wrapped-then-reparsed by the client. Half a day after the Protocol Version bump issue.
Author
Collaborator

Implemented. Three sub-changes in lmcp.lua:

1. Protocol version bump: MCP_VERSION = "2025-06-18".

2. structuredContent (+ outputSchema):

  • :tool() opts now accepts outputSchema = <JSON Schema> next to annotations.
  • tools/list emits outputSchema when set (omitted otherwise).
  • tools/call handler-return path: when handler returns a non-typed table, the dispatch now emits BOTH content: [{type:"text", text: <json>}] (backwards-compat) AND structuredContent: <table> (new). Existing tools fetch and web_search get this for free — clients gain first-class structured access without any tool-side changes.
  • string returns and typed-content ({type="image", ...}) returns are unchanged — no structuredContent emitted.

3. _meta passthrough:

  • Handler signature is now function(args, ctx) where ctx = { _meta = req.params._meta, request_id = id }. Existing 1-arg handlers keep working (Lua silently discards extras).
  • Handler return may include _meta = {...}; the dispatcher echoes it as the response top-level _meta AND strips it from structuredContent so server metadata doesn't leak into the structured payload.

Verified live: protocolVersion is now 2025-06-18; fetch returns both content (JSON text) and structuredContent (parsed object) with same fields; read_file (string return) has no structuredContent; custom test tool confirms ctx._meta receives the request meta and result._meta is echoed on response.

No tool registrations were updated in this change — fetch/web_search/remote_* all benefit additively without code changes.

Implemented. Three sub-changes in lmcp.lua: **1. Protocol version bump:** `MCP_VERSION = "2025-06-18"`. **2. structuredContent (+ outputSchema):** - `:tool()` opts now accepts `outputSchema = <JSON Schema>` next to `annotations`. - `tools/list` emits `outputSchema` when set (omitted otherwise). - `tools/call` handler-return path: when handler returns a non-typed table, the dispatch now emits BOTH `content: [{type:"text", text: <json>}]` (backwards-compat) AND `structuredContent: <table>` (new). Existing tools `fetch` and `web_search` get this for free — clients gain first-class structured access without any tool-side changes. - string returns and typed-content (`{type="image", ...}`) returns are unchanged — no structuredContent emitted. **3. _meta passthrough:** - Handler signature is now `function(args, ctx)` where `ctx = { _meta = req.params._meta, request_id = id }`. Existing 1-arg handlers keep working (Lua silently discards extras). - Handler return may include `_meta = {...}`; the dispatcher echoes it as the response top-level `_meta` AND strips it from `structuredContent` so server metadata doesn't leak into the structured payload. Verified live: protocolVersion is now `2025-06-18`; `fetch` returns both `content` (JSON text) and `structuredContent` (parsed object) with same fields; `read_file` (string return) has no structuredContent; custom test tool confirms `ctx._meta` receives the request meta and `result._meta` is echoed on response. No tool registrations were updated in this change — `fetch`/`web_search`/`remote_*` all benefit additively without code changes.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marfrit/lmcp#13