Skip to content

Refactor: Extract Capabilities object #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 10 additions & 13 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "json_rpc_handler"
require_relative "instrumentation"
require_relative "methods"
require_relative "server/capabilities"

module MCP
class Server
Expand Down Expand Up @@ -45,6 +46,10 @@ def initialize(
@resource_index = index_resources_by_uri(resources)
@server_context = server_context
@configuration = MCP.configuration.merge(configuration)
@capabilities = Capabilities.new(capabilities)
@capabilities.support_tools if tools.any?
@capabilities.support_prompts if prompts.any?
@capabilities.support_resources if resources.any? || resource_templates.any?

@handlers = {
Methods::RESOURCES_LIST => method(:list_resources),
Expand All @@ -66,7 +71,7 @@ def initialize(
end

def capabilities
@capabilities ||= determine_capabilities
@capabilities.to_h
end

def handle(request)
Expand All @@ -92,6 +97,7 @@ def define_prompt(name: nil, description: nil, arguments: [], &block)
end

def resources_list_handler(&block)
@capabilities.support_resources
@handlers[Methods::RESOURCES_LIST] = block
end

Expand All @@ -100,10 +106,12 @@ def resources_read_handler(&block)
end

def resources_templates_list_handler(&block)
@capabilities.support_resources
@handlers[Methods::RESOURCES_TEMPLATES_LIST] = block
end

def tools_list_handler(&block)
@capabilities.support_tools
@handlers[Methods::TOOLS_LIST] = block
end

Expand All @@ -112,6 +120,7 @@ def tools_call_handler(&block)
end

def prompts_list_handler(&block)
@capabilities.support_prompts
@handlers[Methods::PROMPTS_LIST] = block
end

Expand Down Expand Up @@ -159,18 +168,6 @@ def handle_request(request, method)
}
end

def determine_capabilities
defines_prompts = @prompts.any? || @handlers[Methods::PROMPTS_LIST] != method(:list_prompts)
defines_tools = @tools.any? || @handlers[Methods::TOOLS_LIST] != method(:list_tools)
defines_resources = @resources.any? || @handlers[Methods::RESOURCES_LIST] != method(:list_resources)
defines_resource_templates = @resource_templates.any? || @handlers[Methods::RESOURCES_TEMPLATES_LIST] != method(:list_resource_templates)
{
prompts: defines_prompts ? {} : nil,
resources: defines_resources || defines_resource_templates ? {} : nil,
tools: defines_tools ? {} : nil,
}.compact
end

def server_info
@server_info ||= {
name:,
Expand Down
96 changes: 96 additions & 0 deletions lib/mcp/server/capabilities.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# frozen_string_literal: true

module MCP
class Server
class Capabilities
def initialize(capabilities_hash = nil)
@completions = nil
@experimental = 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_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

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,
logging: @logging,
prompts: @prompts,
resources: @resources,
tools: @tools,
}.compact
end
end
end
end
197 changes: 197 additions & 0 deletions test/mcp/server/capabilities_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# frozen_string_literal: true

require "test_helper"

module MCP
class Server
class CapabilitiesTest < ActiveSupport::TestCase
test "can be initialized with a capabilities hash" do
capabilities = MCP::Server::Capabilities.new(prompts: {})

assert_equal({ prompts: {} }, capabilities.to_h)
end

test "ignores unknown values in capabilities hash" do
capabilities = MCP::Server::Capabilities.new(prompts: {}, invalid: :value)

assert_equal({ prompts: {} }, capabilities.to_h)
end

test "support prompts" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_prompts

assert_operator capabilities.to_h, :key?, :prompts
end

test "support resources" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_resources

assert_operator capabilities.to_h, :key?, :resources
end

test "support tools" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_tools

assert_operator capabilities.to_h, :key?, :tools
end

test "support completions" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_completions

assert_operator capabilities.to_h, :key?, :completions
assert_empty(capabilities.to_h[:completions])
end

test "support logging" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_logging

assert_operator capabilities.to_h, :key?, :logging
assert_empty(capabilities.to_h[:logging])
end

test "support experimental with custom config" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_experimental({ myFeature: { enabled: true } })

assert_operator capabilities.to_h, :key?, :experimental
assert_equal({ myFeature: { enabled: true } }, capabilities.to_h[:experimental])
end

test "support prompts list changed" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_prompts_list_changed

assert_equal({ prompts: { listChanged: true } }, capabilities.to_h)
end

test "support resources list changed" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_resources_list_changed

assert_equal({ resources: { listChanged: true } }, capabilities.to_h)
end

test "support resources subscribe" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_resources_subscribe

assert_equal({ resources: { subscribe: true } }, capabilities.to_h)
end

test "support resources with both list changed and subscribe" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_resources_list_changed
capabilities.support_resources_subscribe

assert_equal({ resources: { listChanged: true, subscribe: true } }, capabilities.to_h)
end

test "support tools list changed" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_tools_list_changed

assert_equal({ tools: { listChanged: true } }, capabilities.to_h)
end

test "initializes with prompts list changed from hash" do
capabilities = MCP::Server::Capabilities.new(prompts: { listChanged: true })

assert_equal({ prompts: { listChanged: true } }, capabilities.to_h)
end

test "initializes with resources list changed and subscribe from hash" do
capabilities = MCP::Server::Capabilities.new(resources: { listChanged: true, subscribe: true })

assert_equal({ resources: { listChanged: true, subscribe: true } }, capabilities.to_h)
end

test "initializes with tools list changed from hash" do
capabilities = MCP::Server::Capabilities.new(tools: { listChanged: true })

assert_equal({ tools: { listChanged: true } }, capabilities.to_h)
end

test "initializes with completions from hash" do
capabilities = MCP::Server::Capabilities.new(completions: {})

assert_equal({ completions: {} }, capabilities.to_h)
end

test "initializes with logging from hash" do
capabilities = MCP::Server::Capabilities.new(logging: {})

assert_equal({ logging: {} }, capabilities.to_h)
end

test "initializes with experimental config from hash" do
capabilities = MCP::Server::Capabilities.new(experimental: { feature1: { enabled: true }, feature2: "config" })

assert_equal({ experimental: { feature1: { enabled: true }, feature2: "config" } }, capabilities.to_h)
end

test "initializes with all capabilities from hash" do
capabilities = MCP::Server::Capabilities.new(
completions: {},
experimental: { customFeature: true },
logging: {},
prompts: { listChanged: true },
resources: { listChanged: true, subscribe: true },
tools: { listChanged: true },
)

expected = {
completions: {},
experimental: { customFeature: true },
logging: {},
prompts: { listChanged: true },
resources: { listChanged: true, subscribe: true },
tools: { listChanged: true },
}

assert_equal(expected, capabilities.to_h)
end

test "handles nil values in capabilities hash gracefully" do
capabilities = MCP::Server::Capabilities.new(
prompts: nil,
resources: nil,
tools: nil,
)

assert_equal({ prompts: {}, resources: {}, tools: {} }, capabilities.to_h)
end

test "to_h returns empty hash when no capabilities are set" do
capabilities = MCP::Server::Capabilities.new

assert_empty(capabilities.to_h)
end

test "calling support methods multiple times does not duplicate entries" do
capabilities = MCP::Server::Capabilities.new

capabilities.support_prompts
capabilities.support_prompts
capabilities.support_prompts_list_changed

assert_equal({ prompts: { listChanged: true } }, capabilities.to_h)
end
end
end
end