Resources

MCP spec includes Resources.

Defining Resources

Like tools and prompts, resources can be defined in three ways.

1. As a class definition

Define a class that inherits from MCP::Resource, implementing contents to serve the resource body:

class MyResource < MCP::Resource
  uri "https://example.com/my_resource"
  resource_name "my-resource"
  title "My Resource"
  description "Lorem ipsum dolor sit amet"
  mime_type "text/html"

  class << self
    def contents
      [MCP::Resource::TextContents.new(
        uri: uri,
        mime_type: mime_type,
        text: "Hello from example resource!"
      )]
    end
  end
end

server = MCP::Server.new(
  name: "my_server",
  resources: [MyResource],
)

resources/read requests are routed automatically: when the requested URI matches a registered class-based resource, its contents method is called. contents may return an array of MCP::Resource::TextContents / MCP::Resource::BlobContents objects (or plain hashes), or a single one. Like tools, contents can opt in to a server_context: keyword argument to receive per-request context.

When class-based resources or resource templates are registered and a resources/read request does not match any of them, the server responds with the standard JSON-RPC Invalid Params error (-32602) carrying the requested URI in the error data member, per SEP-2164.

2. Using the MCP::Resource.define method

The block implements contents:

resource = MCP::Resource.define(
  uri: "https://example.com/my_resource",
  name: "my-resource",
  mime_type: "text/html",
) do
  [MCP::Resource::TextContents.new(uri: uri, mime_type: mime_type, text: "Hello!")]
end

3. Using the MCP::Server#define_resource method

MCP::Server#define_resource registers the resource directly on a server instance:

server = MCP::Server.new(name: "my_server")
server.define_resource(
  uri: "https://example.com/my_resource",
  name: "my-resource",
  mime_type: "text/html",
) do
  [MCP::Resource::TextContents.new(uri: "https://example.com/my_resource", mime_type: "text/html", text: "Hello!")]
end

Alternatively, resources can be registered as plain data objects with MCP::Resource.new, in which case the server only lists them:

resource = MCP::Resource.new(
  uri: "https://example.com/my_resource",
  name: "my-resource",
  title: "My Resource",
  description: "Lorem ipsum dolor sit amet",
  mime_type: "text/html",
)

server = MCP::Server.new(
  name: "my_server",
  resources: [resource],
)

With plain data resources, the server must register a handler for the resources/read method to retrieve a resource dynamically.

server.resources_read_handler do |params|
  [{
    uri: params[:uri],
    mimeType: "text/plain",
    text: "Hello from example resource! URI: #{params[:uri]}"
  }]
end

otherwise resources/read requests will be a no-op. Note that a resources_read_handler fully replaces the default resources/read handling, including the automatic routing to class-based resources described above.

To make the resource list depend on the request, register a resources_list_handler. The block returns the resource collection to serve, so the visible resources can vary by the authenticated principal or the granted scope. The framework paginates the returned array and stamps the same cache hints it applies to the constructor-provided resources, so the block returns only the array. A block that declares server_context: receives it:

server.resources_list_handler do |params, server_context:|
  server_context[:authenticated] ? real_resources : demo_resources
end

The block is invoked once per page, so it must return a stable ordering across the pages of one query; the cursor is a positional offset into the returned collection. When no handler is set, the resources passed to MCP::Server.new are served unchanged.

For unknown URIs, raise MCP::Server::ResourceNotFoundError from the handler. Per SEP-2164, the server then responds with the standard JSON-RPC Invalid Params error (-32602) carrying the requested URI in the error data member:

server.resources_read_handler do |params|
  resource = lookup(params[:uri])
  raise MCP::Server::ResourceNotFoundError.new(params[:uri], params) unless resource

  [{ uri: params[:uri], mimeType: resource.mime_type, text: resource.body }]
end

Reading Binary Resources

For binary resources, respond with a base64-encoded blob field instead of text. The MCP::Resource::TextContents and MCP::Resource::BlobContents classes build the two contents shapes defined by the spec:

server.resources_read_handler do |params|
  case params[:uri]
  when "file:///logo.png"
    [
      MCP::Resource::BlobContents.new(
        uri: params[:uri],
        mime_type: "image/png",
        data: Base64.strict_encode64(File.binread("logo.png")),
      ).to_h,
    ]
  else
    [
      MCP::Resource::TextContents.new(
        uri: params[:uri],
        mime_type: "text/plain",
        text: "Hello from example resource!",
      ).to_h,
    ]
  end
end

Resource Templates

Resource templates follow the same pattern. Class-based templates declare a uri_template and receive the variables extracted from the requested URI as keyword arguments to contents:

class UserProfileTemplate < MCP::ResourceTemplate
  uri_template "users://{user_id}/profile"
  resource_template_name "user-profile"
  title "User Profile"
  description "Profile data for a user"
  mime_type "application/json"

  class << self
    def contents(user_id:)
      [MCP::Resource::TextContents.new(
        uri: "users://#{user_id}/profile",
        mime_type: mime_type,
        text: { id: user_id }.to_json
      )]
    end
  end
end

server = MCP::Server.new(
  name: "my_server",
  resource_templates: [UserProfileTemplate],
)

A resources/read request for users://42/profile calls UserProfileTemplate.contents(user_id: "42"). An exact match against a registered resource takes precedence over template matching. contents can also opt in to a server_context: keyword argument.

URI template matching supports simple RFC 6570 level 1 {variable} expressions only:

  • Operator expressions such as {+path}, {#fragment}, or {?query} are treated as literal text and never match an expanded URI.
  • A variable matches one or more characters excluding /.
  • Extracted values are not percent-decoded.

The MCP::ResourceTemplate.define and MCP::Server#define_resource_template methods are also available, mirroring the resource variants:

server.define_resource_template(
  uri_template: "users://{user_id}/profile",
  name: "user-profile",
  mime_type: "application/json",
) do |user_id:|
  [MCP::Resource::TextContents.new(
    uri: "users://#{user_id}/profile",
    mime_type: "application/json",
    text: { id: user_id }.to_json
  )]
end

Resource templates can also be registered as plain data objects with MCP::ResourceTemplate.new, in which case reads must be served by a resources_read_handler:

resource_template = MCP::ResourceTemplate.new(
  uri_template: "https://example.com/my_resource_template",
  name: "my-resource-template",
  title: "My Resource Template",
  description: "Lorem ipsum dolor sit amet",
  mime_type: "text/html",
)

server = MCP::Server.new(
  name: "my_server",
  resource_templates: [resource_template],
)

Registered templates are listed through the resources/templates/list protocol method. To serve reads for URIs that match a template, extract the variable parts of the URI in your resources_read_handler:

resource_template = MCP::ResourceTemplate.new(
  uri_template: "file:///items/{item_id}",
  name: "item",
  mime_type: "application/json",
)

server = MCP::Server.new(name: "my_server", resource_templates: [resource_template])

server.resources_read_handler do |params|
  if (match = params[:uri].match(%r{\Afile:///items/(?<item_id>[^/]+)\z}))
    [{
      uri: params[:uri],
      mimeType: "application/json",
      text: { id: match[:item_id] }.to_json,
    }]
  else
    raise MCP::Server::ResourceNotFoundError.new(params[:uri], params)
  end
end

Resource Subscriptions

Resource subscriptions allow clients to monitor specific resources for changes. When a subscribed resource is updated, the server sends a notification to the client.

The SDK does not track subscription state internally. Server developers register handlers and manage their own subscription state. Three methods are provided:

  • Server#resources_subscribe_handler - registers a handler for resources/subscribe requests
  • Server#resources_unsubscribe_handler - registers a handler for resources/unsubscribe requests
  • ServerContext#notify_resources_updated - sends a notifications/resources/updated notification to the subscribing client
subscribed_uris = Set.new

server = MCP::Server.new(
  name: "my_server",
  resources: [my_resource],
  capabilities: { resources: { subscribe: true } },
)

server.resources_subscribe_handler do |params|
  subscribed_uris.add(params[:uri].to_s)
end

server.resources_unsubscribe_handler do |params|
  subscribed_uris.delete(params[:uri].to_s)
end

server.define_tool(name: "update_resource") do |server_context:, **args|
  if subscribed_uris.include?("test://my-resource")
    server_context.notify_resources_updated(uri: "test://my-resource")
  end
  MCP::Tool::Response.new([MCP::Content::Text.new("Resource updated").to_h])
end

The resources/subscribe and resources/unsubscribe responses are empty results. The one field the spec allows alongside is _meta, so a handler that returns { _meta: { ... } } has it passed through; any other field it returns is dropped. To convey a subscription identifier or other advisory data to the client, nest it under _meta rather than returning it at the top level, which interoperating clients reject:

server.resources_subscribe_handler do |params|
  id = subscriptions.create(params[:uri].to_s)
  { _meta: { "myapp.example/subscriptionId" => id } }
end