Skip to content

Proposal: dynamic/lazy resources/list (and directory-read) backing for gateway/proxy and template-served catalogs #998

@sambhav

Description

@sambhav

Problem

resources/list can only be served from a static, in-memory set. Resources are materialized up front via AddResource (removed via RemoveResources), and (*Server).listResources enumerates that featureSet directly — there is no handler/callback that computes the list on demand. For two important classes of server this isn't just inconvenient, it's a structural mismatch.

1. Gateway / proxy servers (the important case)

A skill (or resource) gateway fronts one or more upstream catalogs it does not own — another registry, a remote index, or several backend MCP servers. The authoritative list lives elsewhere and changes independently of this process. To serve resources/list from a static set, the gateway would have to mirror the entire upstream catalog into memory at startup and keep it continuously in sync — which defeats the purpose of being a gateway, doesn't scale to large/unbounded upstreams, and is racy against upstream changes.

What a gateway actually needs is to answer resources/list by querying upstream on demand, passing the client's pagination cursor through. There is no first-class way to do that today.

2. Template-served assets are non-enumerable by construction

Skills are served as a directory of files. A server typically exposes a skill's files via a resource template (skill://<skill>/{+path}) plus one read handler, so it doesn't have to AddResource every individual file. But a URI template matches infinitely many URIs — you cannot derive the file list from the template definition. So those assets are fully readable yet absent from any static set: resources/list won't show them, and resources/directory/read (below) backed by the static set returns empty.

Relationship to resources/directory/read (#956 / SEP-2640)

The Skills-over-MCP WG is adding a resources/directory/read method (SEP-2640) so hosts can enumerate the files within a skill directory — e.g. to materialize a skill to disk. Implementing that verb in this SDK depends on custom-method support (#956). But even once the verb exists, it hits exactly this problem: for a template-served or gateway-proxied skill, the directory's children cannot come from the static set — resources/directory/read must be backed by a server-provided dynamic listing.

So the two are a pair:

Neither alone lets a Go skills/gateway server both serve and enumerate a catalog whose contents aren't pre-materialized.

What already works (to scope precisely)

resources/read is already dynamic — content comes from a ResourceHandler, and resource templates (AddResourceTemplate + ResourceTemplate.Matches) let one handler serve an unbounded URI space. The asymmetry is the whole point: templates make reads unbounded, but nothing makes listing/enumeration unbounded or on-demand.

Workarounds today (and why they fall short)

  1. Receiving middleware. Middleware wraps MethodHandler func(ctx, method string, req Request)(Result, error), so middleware can intercept resources/list and synthesize a *ListResourcesResult, short-circuiting the static handler. It works, but it's untyped (switch on a string method, type-assert req/Result), bypasses the SDK's built-in pagination, and sits awkwardly beside the otherwise-typed registration model.
  2. Per-session server via the getServer callback (as suggested in Support for per session dynamic tools/prompts #216). Good for per-connection configuration, but it still requires materializing a concrete set per server — it doesn't help a catalog that is upstream, generated, or too large to enumerate eagerly.

Proposal

Add a first-class way to serve resources/list (and back resources/directory/read) from a handler, with cursor-based pagination handled by the SDK — analogous to how ResourceHandler/templates already make reads dynamic. Illustrative shape (not prescriptive):

// Serves resources/list on demand; receives the request (incl. pagination cursor)
// and returns a page of resources plus the next cursor.
type ListResourcesHandler func(context.Context, *ListResourcesRequest) (*ListResourcesResult, error)

opts := &mcp.ServerOptions{
    ListResourcesHandler: func(ctx context.Context, req *mcp.ListResourcesRequest) (*mcp.ListResourcesResult, error) {
        // e.g. proxy upstream using req.Params.Cursor, or enumerate a template's namespace ...
    },
}

When set, the handler would take precedence over (or compose with) the registered static set, and the same mechanism would back directory enumeration for a directory URI.

Questions for maintainers

  • Was the static-set model for resources/list a deliberate choice (uniformity with tools/prompts feature sets), and would a dynamic list handler be acceptable — or is middleware / per-session-server the intended long-term answer for gateway and unbounded catalogs?
  • If a handler is acceptable: should it replace or merge with the registered set, and should the same backing apply to resources/directory/read (mcp: Allow registration of custom JSON-RPC methods #956) and, for consistency, prompts/list / tools/list?

Happy to prototype if there's interest in the direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions