ruby-sdk/lib/mcp/server/capabilities.rb
Koichi ITO 2c62139154 Support Capability Extensions per SEP-2133
## Motivation and Context

SEP-2133 (modelcontextprotocol/modelcontextprotocol#2133, merged for
the 2026-07-28 spec release) establishes the extensions framework for MCP:
both `ClientCapabilities` and `ServerCapabilities` gain an optional `extensions`
member keyed by reverse-DNS extension identifiers
(e.g. `"io.modelcontextprotocol/tasks"`, `"com.example/feature"`), with arbitrary
extension-defined configuration objects as values and an empty object meaning
"supported with no settings". Official extensions such as MCP Apps (SEP-1865)
and the Tasks extension (SEP-2663) are layered on this field.

This mirrors the shape the TypeScript SDK merged (typescript-sdk#1630):
plain declaration and passthrough, no registrar API and no key-format validation
(the naming convention is documented instead).

- `MCP::Server::Capabilities` gains `support_extensions` (repeated calls merge),
  accepts an `:extensions` key in its constructor hash, and includes `extensions`
  in `to_h`. The class was previously dead code: it was never required and
  `Server.new` only took plain hashes. It is now required by `lib/mcp/server.rb`,
  and `Server.new(capabilities:)` accepts either a plain Hash (as before) or
  a `Capabilities` instance.
- Server-declared extensions appear in the `initialize` result;
  extensions the client declares during `initialize` were already stored verbatim and
  are readable via `Server#client_capabilities[:extensions]` and
  `ServerSession#client_capabilities[:extensions]` (now covered by tests).
- `MCP::Client#connect(capabilities:)` already passes capabilities through verbatim;
  its documentation now covers the `extensions` member.

Resolves #378.

## How Has This Been Tested?

- New `test/mcp/server/capabilities_test.rb` covers the previously untested
  `Capabilities` class: empty default, constructor round-trip, extensions via
  constructor and `support_extensions`, merge semantics, nil tolerance,
  and omission from `to_h` when never declared.
- New tests in `test/mcp/server_test.rb` assert that the `initialize` result
  carries declared extensions (from both a plain hash and a `Capabilities` instance)
  and that client-declared extensions are readable via `Server#client_capabilities`
  and `ServerSession#client_capabilities`.

## Breaking Changes

None. `extensions` is additive and appears on the wire only when declared;
servers constructed with plain capability hashes behave exactly as before.
2026-06-14 17:20:45 +09:00

110 lines
3.2 KiB
Ruby

# frozen_string_literal: true
module MCP
class Server
class Capabilities
def initialize(capabilities_hash = nil)
@completions = nil
@experimental = nil
@extensions = nil
@logging = nil
@prompts = nil
@resources = nil
@tools = nil
if capabilities_hash
support_completions if capabilities_hash.key?(:completions)
support_experimental(capabilities_hash[:experimental]) if capabilities_hash.key?(:experimental)
support_extensions(capabilities_hash[:extensions]) if capabilities_hash.key?(:extensions)
support_logging if capabilities_hash.key?(:logging)
if capabilities_hash.key?(:prompts)
support_prompts
prompts_config = capabilities_hash[:prompts] || {}
support_prompts_list_changed if prompts_config[:listChanged]
end
if capabilities_hash.key?(:resources)
support_resources
resources_config = capabilities_hash[:resources] || {}
support_resources_list_changed if resources_config[:listChanged]
support_resources_subscribe if resources_config[:subscribe]
end
if capabilities_hash.key?(:tools)
support_tools
tools_config = capabilities_hash[:tools] || {}
support_tools_list_changed if tools_config[:listChanged]
end
end
end
def support_completions
@completions ||= {}
end
def support_experimental(config = {})
@experimental = config || {}
end
# Declares support for capability extensions per SEP-2133. Keys are
# extension identifiers using the reverse-DNS prefix convention
# (e.g. `"io.modelcontextprotocol/tasks"`, `"com.example/feature"`);
# values are arbitrary extension-defined configuration objects,
# with an empty hash meaning "supported with no settings".
# Repeated calls merge, so several extensions can be declared independently.
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133
def support_extensions(extensions = {})
@extensions = (@extensions || {}).merge(extensions || {})
end
def support_logging
@logging ||= {}
end
def support_prompts
@prompts ||= {}
end
def support_prompts_list_changed
support_prompts
@prompts[:listChanged] = true
end
def support_resources
@resources ||= {}
end
def support_resources_list_changed
support_resources
@resources[:listChanged] = true
end
def support_resources_subscribe
support_resources
@resources[:subscribe] = true
end
def support_tools
@tools ||= {}
end
def support_tools_list_changed
support_tools
@tools[:listChanged] = true
end
def to_h
{
completions: @completions,
experimental: @experimental,
extensions: @extensions,
logging: @logging,
prompts: @prompts,
resources: @resources,
tools: @tools,
}.compact
end
end
end
end