Add a resources_list_handler for context-dependent resource lists

## Motivation and Context

`resources/read` can be taken over with `resources_read_handler`, but `resources/list` had no equivalent:
it was bound to the internal `list_resources`, which paginates the constructor-provided `resources` array,
and `define_custom_method` refuses to rebind a method the server already handles.
An application whose visible resources depend on the request - authenticated versus anonymous sessions,
or an OAuth-scoped connector that must see a scope-filtered set - had to build a fresh `Server` per request
or override a private method.

Both reference SDKs let the list vary per request. The TypeScript SDK exposes `resources/list` through
`setRequestHandler` and gives resource templates a per-context `listCallback(ctx)`; the Python SDK takes
an `on_list_resources` handler. This adds the Ruby equivalent.

`resources_list_handler(&block)` registers a block that returns the resource collection to serve.
It feeds `list_resources` rather than replacing it, so the framework still paginates the returned array
and stamps the SEP-2549 cache hints; the block returns only the array, matching how `resources_read_handler` returns
only the contents and the framework wraps them. A block that declares a `server_context:` keyword receives
an `MCP::ServerContext`, reusing the same opt-in rule the read handler uses, so the list can be filtered by
the authenticated principal or granted scope. The block is invoked once per page and must return
a stable ordering across the pages of one query, since the cursor is a positional offset. When no handler is set,
the constructor-provided resources are served unchanged.

The `resources` capability is advertised whether or not the constructor array is populated, so a server that
provides only a `resources_list_handler` still answers `resources/list`.

Fixes #507.

## How Has This Been Tested?

New tests in `test/mcp/server_test.rb` cover a handler replacing the served collection, a handler receiving `server_context:`
and returning different sets for authenticated and anonymous requests, pagination and cache hints still applying to
the handler-provided array, and a server with no constructor-provided resources served entirely by the handler.
The existing `resources/list` test, which exercises the no-handler default path, is unchanged.

## Breaking Changes

None. When no `resources_list_handler` is set, `resources/list` serves the constructor-provided resources exactly as before.
This commit is contained in:
Koichi ITO 2026-08-13 00:10:27 +09:00
commit 27044b0047

View file

@ -182,6 +182,7 @@ module MCP
@resources = resources
@resource_templates = resource_templates
@resource_index = index_resources_by_uri(resources)
@resources_list_handler = nil
@server_context = server_context
self.page_size = page_size
self.ttl_ms = ttl_ms
@ -387,6 +388,22 @@ module MCP
@handlers[Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED] = block
end
# Sets a custom handler for `resources/list` requests, letting the visible list depend on request context such as
# the authenticated principal or granted scope. The block returns the resource collection to serve;
# the framework paginates it and stamps SEP-2549 cache hints exactly as it does for the constructor-provided resources,
# so the block returns only the array, not the paginated result.
# A block that declares a `server_context:` keyword receives an `MCP::ServerContext`. When no handler is set,
# the constructor-provided `resources` array is served unchanged.
#
# The block is invoked once per page, so it must return a stable ordering across the pages of one logical query;
# the cursor is a positional offset into the returned collection.
#
# @yield [params, server_context:] The request params, and an `MCP::ServerContext` when declared.
# @yieldreturn [Array<MCP::Resource>] The resources to paginate.
def resources_list_handler(&block)
@resources_list_handler = block
end
# Sets a custom handler for `resources/read` requests.
# The block receives the parsed request params and should return resource
# contents. The return value is set as the `contents` field of the response.
@ -1088,12 +1105,28 @@ module MCP
call_prompt_template_with_args(prompt, prompt_args, server_context)
end
def list_resources(request)
page = paginate(@resources, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
def list_resources(request, server_context: nil)
resources = if @resources_list_handler
invoke_resources_list_handler(request, server_context)
else
@resources
end
page = paginate(resources, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
apply_cache_metadata({ resources: page[:items], nextCursor: page[:next_cursor] }.compact)
end
# Calls the `resources_list_handler` block, forwarding `server_context:` only when the block opts in
# by declaring the keyword (the same rule `dispatch_optional_context_handler` applies).
def invoke_resources_list_handler(request, server_context)
if handler_declares_server_context?(@resources_list_handler)
@resources_list_handler.call(request, server_context: server_context)
else
@resources_list_handler.call(request)
end
end
# Default `resources/read` handler: routes to class-based resources and resource templates.
# Fully replaced when `resources_read_handler` is set. When no class-based resource or template is registered,
# unknown URIs keep the historical no-op `[]` response instead of raising.