Add resources primitive (resources/list, resources/read, templates) #5

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

Add the Resources primitive — server exposes addressable, read-only content (files, URIs, generated views) that clients can list, read, and reference in prompts without going through a tool call. This is the largest single missing surface vs. the MCP spec (2025-06-18).

Goal

Let an MCP client browse and read content the server owns, the same way it browses tools today. Tools-only servers force the client to spend a tools/call round-trip for every read, with no caching key the client can use. Resources fix that with stable URIs and explicit text/binary types.

Methods to add

Method Notes
resources/list Returns { resources: [{ uri, name, description?, mimeType? }], nextCursor? }. Cursor-paginated.
resources/read Args { uri }{ contents: [{ uri, mimeType, text | blob }] }. blob is base64 for binary.
resources/templates/list Returns parameterised URI templates (e.g. file:///{path}) clients can fill in.
notifications/resources/list_changed Sent when the registered set changes.

Defer to a v2 (separate issue): resources/subscribe, resources/unsubscribe, notifications/resources/updated. They require server-initiated transport (depends on the Streamable-HTTP issue).

API for lmcp (Lua surface)

Mirror the existing server:tool(name, desc, schema, handler) shape:

server:resource("file:///etc/hosts", {
    name = "hosts file",
    mimeType = "text/plain",
}, function() return io.open("/etc/hosts"):read('*a') end)

server:resource_template("file:///{path}", {
    name = "any host file",
    mimeType = "text/plain",
}, function(args) return io.open(args.path):read('*a') end)

Handler return convention: string → single text content; table with blob/mimeType → binary content; raise → JSON-RPC error. Matches the existing tool handler ergonomics.

Capabilities advertisement

initialize response gains:

capabilities = {
    tools = { listChanged = false },
    resources = { listChanged = true, subscribe = false },  -- subscribe in v2
}

Scope (v1)

  • resources/list, resources/read, resources/templates/list.
  • notifications/resources/list_changed if a resource is registered after init.
  • Cursor pagination on resources/list (even if lmcp returns everything in one page today — keeps the contract spec-conformant).

Out of scope (separate issues)

  • resources/subscribe + notifications/resources/updated — needs server-initiated SSE.
  • _meta field passthrough — covered by the "structured tool output" issue.

Priority

High — biggest single MCP-spec gap in lmcp. Unlocks a class of client patterns (resource-pickers, attach-to-prompt UI, cached reads) that tools-only servers can't reach. Estimated 1 day.

Add the **Resources** primitive — server exposes addressable, read-only content (files, URIs, generated views) that clients can list, read, and reference in prompts without going through a tool call. This is the largest single missing surface vs. the MCP spec (`2025-06-18`). ## Goal Let an MCP client browse and read content the server owns, the same way it browses tools today. Tools-only servers force the client to spend a `tools/call` round-trip for every read, with no caching key the client can use. Resources fix that with stable URIs and explicit text/binary types. ## Methods to add | Method | Notes | |---|---| | `resources/list` | Returns `{ resources: [{ uri, name, description?, mimeType? }], nextCursor? }`. Cursor-paginated. | | `resources/read` | Args `{ uri }` → `{ contents: [{ uri, mimeType, text \| blob }] }`. `blob` is base64 for binary. | | `resources/templates/list` | Returns parameterised URI templates (e.g. `file:///{path}`) clients can fill in. | | `notifications/resources/list_changed` | Sent when the registered set changes. | Defer to a v2 (separate issue): `resources/subscribe`, `resources/unsubscribe`, `notifications/resources/updated`. They require server-initiated transport (depends on the Streamable-HTTP issue). ## API for lmcp (Lua surface) Mirror the existing `server:tool(name, desc, schema, handler)` shape: ```lua server:resource("file:///etc/hosts", { name = "hosts file", mimeType = "text/plain", }, function() return io.open("/etc/hosts"):read('*a') end) server:resource_template("file:///{path}", { name = "any host file", mimeType = "text/plain", }, function(args) return io.open(args.path):read('*a') end) ``` Handler return convention: string → single text content; table with `blob`/`mimeType` → binary content; raise → JSON-RPC error. Matches the existing tool handler ergonomics. ## Capabilities advertisement `initialize` response gains: ``` capabilities = { tools = { listChanged = false }, resources = { listChanged = true, subscribe = false }, -- subscribe in v2 } ``` ## Scope (v1) - `resources/list`, `resources/read`, `resources/templates/list`. - `notifications/resources/list_changed` if a resource is registered after init. - Cursor pagination on `resources/list` (even if lmcp returns everything in one page today — keeps the contract spec-conformant). ## Out of scope (separate issues) - `resources/subscribe` + `notifications/resources/updated` — needs server-initiated SSE. - `_meta` field passthrough — covered by the "structured tool output" issue. ## Priority **High** — biggest single MCP-spec gap in lmcp. Unlocks a class of client patterns (resource-pickers, attach-to-prompt UI, cached reads) that tools-only servers can't reach. Estimated 1 day.
Author
Collaborator

Implemented in lmcp.lua. Resources primitive: server:resource(uri, opts, handler) + server:resource_template(uriTemplate, opts, handler); resources/list, resources/read, resources/templates/list dispatch; notifications/resources/list_changed queue (delivery NYI per #16); template captures via (.+) greedy substitution; binary blob auto-base64 via mime.b64; -32002 for not-found, -32602 for invalid params, -32603 for handler errors.

Capability advertised iff a resource/template is registered OR opts.resources = true (opt-in for late registration).

Verified live: capability emission, text/binary/template reads, error paths, backwards compat preserved. Sample resources in example_server.lua: text://greeting, data://lmcp.png, file:///{path}.

Memory: project_json_empty_table_gotcha.md captures the {}[] encoding trap that bit notification.params — workaround was to omit params entirely. Later issue #19 added the json.empty_object sentinel for cases where omission isn't an option.

Implemented in lmcp.lua. Resources primitive: `server:resource(uri, opts, handler)` + `server:resource_template(uriTemplate, opts, handler)`; `resources/list`, `resources/read`, `resources/templates/list` dispatch; `notifications/resources/list_changed` queue (delivery NYI per #16); template captures via `(.+)` greedy substitution; binary blob auto-base64 via `mime.b64`; -32002 for not-found, -32602 for invalid params, -32603 for handler errors. Capability advertised iff a resource/template is registered OR `opts.resources = true` (opt-in for late registration). Verified live: capability emission, text/binary/template reads, error paths, backwards compat preserved. Sample resources in example_server.lua: `text://greeting`, `data://lmcp.png`, `file:///{path}`. Memory: project_json_empty_table_gotcha.md captures the `{}` → `[]` encoding trap that bit notification.params — workaround was to omit `params` entirely. Later issue #19 added the `json.empty_object` sentinel for cases where omission isn't an option.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marfrit/lmcp#5