From e4c9644e787055b8f119a71b63d66689568f92c0 Mon Sep 17 00:00:00 2001 From: Kevin Fischer Date: Sat, 14 Jun 2025 17:29:16 +0900 Subject: [PATCH 1/5] Refactor ensure capability tests --- test/mcp/methods_test.rb | 91 ++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 55 deletions(-) diff --git a/test/mcp/methods_test.rb b/test/mcp/methods_test.rb index 28e3f050..17e99b8e 100644 --- a/test/mcp/methods_test.rb +++ b/test/mcp/methods_test.rb @@ -5,71 +5,52 @@ module MCP class MethodsTest < ActiveSupport::TestCase - test "ensure_capability! for tools/list method raises an error if tools capability is not present" do - error = assert_raises(Methods::MissingRequiredCapabilityError) do - Methods.ensure_capability!(Methods::TOOLS_LIST, {}) + class << self + def ensure_capability_raises_error_for(method, required_capability_name:, capabilities: {}) + test("ensure_capability! for #{method} raises an error if #{required_capability_name} capability is not present") do + error = assert_raises(Methods::MissingRequiredCapabilityError) do + Methods.ensure_capability!(method, capabilities) + end + assert_equal("Server does not support #{required_capability_name} (required for #{method})", error.message) + end end - assert_equal "Server does not support tools (required for tools/list)", error.message - end - test "ensure_capability! for sampling/createMessage raises an error if sampling capability is not present" do - error = assert_raises(Methods::MissingRequiredCapabilityError) do - Methods.ensure_capability!(Methods::SAMPLING_CREATE_MESSAGE, {}) + def ensure_capability_does_not_raise_for(method, capabilities: {}) + test("ensure_capability! does not raise for #{method}") do + assert_nothing_raised { Methods.ensure_capability!(method, capabilities) } + end end - assert_equal "Server does not support sampling (required for sampling/createMessage)", error.message end - test "ensure_capability! for completion/complete raises an error if completions capability is not present" do - error = assert_raises(Methods::MissingRequiredCapabilityError) do - Methods.ensure_capability!(Methods::COMPLETION_COMPLETE, {}) - end - assert_equal "Server does not support completions (required for completion/complete)", error.message - end + # Tools capability tests + ensure_capability_raises_error_for Methods::TOOLS_LIST, required_capability_name: "tools" + ensure_capability_raises_error_for Methods::TOOLS_CALL, required_capability_name: "tools" - test "ensure_capability! for logging/setLevel raises an error if logging capability is not present" do - error = assert_raises(Methods::MissingRequiredCapabilityError) do - Methods.ensure_capability!(Methods::LOGGING_SET_LEVEL, {}) - end - assert_equal "Server does not support logging (required for logging/setLevel)", error.message - end + # Sampling capability tests + ensure_capability_raises_error_for Methods::SAMPLING_CREATE_MESSAGE, required_capability_name: "sampling" - test "ensure_capability! for prompts/get and prompts/list raise an error if prompts capability is not present" do - [Methods::PROMPTS_GET, Methods::PROMPTS_LIST].each do |method| - error = assert_raises(Methods::MissingRequiredCapabilityError) do - Methods.ensure_capability!(method, {}) - end - assert_equal "Server does not support prompts (required for #{method})", error.message - end - end + # Completions capability tests + ensure_capability_raises_error_for Methods::COMPLETION_COMPLETE, required_capability_name: "completions" - test "ensure_capability! for resources/list, resources/templates/list, resources/read raise an error if resources capability is not present" do - [Methods::RESOURCES_LIST, Methods::RESOURCES_TEMPLATES_LIST, Methods::RESOURCES_READ].each do |method| - error = assert_raises(Methods::MissingRequiredCapabilityError) do - Methods.ensure_capability!(method, {}) - end - assert_equal "Server does not support resources (required for #{method})", error.message - end - end + # Logging capability tests + ensure_capability_raises_error_for Methods::LOGGING_SET_LEVEL, required_capability_name: "logging" - test "ensure_capability! for tools/call and tools/list raise an error if tools capability is not present" do - [Methods::TOOLS_CALL, Methods::TOOLS_LIST].each do |method| - error = assert_raises(Methods::MissingRequiredCapabilityError) do - Methods.ensure_capability!(method, {}) - end - assert_equal "Server does not support tools (required for #{method})", error.message - end - end + # Prompts capability tests + ensure_capability_raises_error_for Methods::PROMPTS_GET, required_capability_name: "prompts" + ensure_capability_raises_error_for Methods::PROMPTS_LIST, required_capability_name: "prompts" - test "ensure_capability! for resources/subscribe raises an error if resources subscribe capability is not present" do - error = assert_raises(Methods::MissingRequiredCapabilityError) do - Methods.ensure_capability!(Methods::RESOURCES_SUBSCRIBE, { resources: {} }) - end - assert_equal "Server does not support resources_subscribe (required for resources/subscribe)", error.message - end + # Resources capability tests + ensure_capability_raises_error_for Methods::RESOURCES_LIST, required_capability_name: "resources" + ensure_capability_raises_error_for Methods::RESOURCES_TEMPLATES_LIST, required_capability_name: "resources" + ensure_capability_raises_error_for Methods::RESOURCES_READ, required_capability_name: "resources" - test "ensure_capability! does not raise for ping and initialize methods" do - assert_nothing_raised { Methods.ensure_capability!(Methods::PING, {}) } - assert_nothing_raised { Methods.ensure_capability!(Methods::INITIALIZE, {}) } - end + # Resources subscribe capability tests + ensure_capability_raises_error_for Methods::RESOURCES_SUBSCRIBE, + required_capability_name: "resources_subscribe", + capabilities: { resources: {} } + + # Methods that don't require capabilities + ensure_capability_does_not_raise_for Methods::PING + ensure_capability_does_not_raise_for Methods::INITIALIZE end end From 127f12f7409b659e9a38e5c8c6c59773fe0356e9 Mon Sep 17 00:00:00 2001 From: Kevin Fischer Date: Sun, 15 Jun 2025 17:19:58 +0900 Subject: [PATCH 2/5] Add missing method constants --- lib/mcp/methods.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/mcp/methods.rb b/lib/mcp/methods.rb index 9a2db26b..e6c0ea19 100644 --- a/lib/mcp/methods.rb +++ b/lib/mcp/methods.rb @@ -19,12 +19,18 @@ module Methods TOOLS_CALL = "tools/call" TOOLS_LIST = "tools/list" + ROOTS_LIST = "roots/list" SAMPLING_CREATE_MESSAGE = "sampling/createMessage" # Notification methods NOTIFICATIONS_TOOLS_LIST_CHANGED = "notifications/tools/list_changed" NOTIFICATIONS_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed" NOTIFICATIONS_RESOURCES_LIST_CHANGED = "notifications/resources/list_changed" + NOTIFICATIONS_RESOURCES_UPDATED = "notifications/resources/updated" + NOTIFICATIONS_ROOTS_LIST_CHANGED = "notifications/roots/list_changed" + NOTIFICATIONS_MESSAGE = "notifications/message" + NOTIFICATIONS_PROGRESS = "notifications/progress" + NOTIFICATIONS_CANCELLED = "notifications/cancelled" class MissingRequiredCapabilityError < StandardError attr_reader :method From 9a00406fae844ecf5cbc5fce9756d6409e4e36e4 Mon Sep 17 00:00:00 2001 From: Kevin Fischer Date: Sun, 15 Jun 2025 17:21:42 +0900 Subject: [PATCH 3/5] Rewrite extend self to class << self --- lib/mcp/methods.rb | 66 +++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/mcp/methods.rb b/lib/mcp/methods.rb index e6c0ea19..fa7b9d93 100644 --- a/lib/mcp/methods.rb +++ b/lib/mcp/methods.rb @@ -43,41 +43,41 @@ def initialize(method, capability) end end - extend self + class << self + def ensure_capability!(method, capabilities) + case method + when PROMPTS_GET, PROMPTS_LIST + unless capabilities[:prompts] + raise MissingRequiredCapabilityError.new(method, :prompts) + end + when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ, RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE + unless capabilities[:resources] + raise MissingRequiredCapabilityError.new(method, :resources) + end - def ensure_capability!(method, capabilities) - case method - when PROMPTS_GET, PROMPTS_LIST - unless capabilities[:prompts] - raise MissingRequiredCapabilityError.new(method, :prompts) + if method == RESOURCES_SUBSCRIBE && !capabilities[:resources][:subscribe] + raise MissingRequiredCapabilityError.new(method, :resources_subscribe) + end + when TOOLS_CALL, TOOLS_LIST + unless capabilities[:tools] + raise MissingRequiredCapabilityError.new(method, :tools) + end + when SAMPLING_CREATE_MESSAGE + unless capabilities[:sampling] + raise MissingRequiredCapabilityError.new(method, :sampling) + end + when COMPLETION_COMPLETE + unless capabilities[:completions] + raise MissingRequiredCapabilityError.new(method, :completions) + end + when LOGGING_SET_LEVEL + # Logging is unsupported by the Server + unless capabilities[:logging] + raise MissingRequiredCapabilityError.new(method, :logging) + end + when INITIALIZE, PING + # No specific capability required for initialize or ping end - when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ, RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE - unless capabilities[:resources] - raise MissingRequiredCapabilityError.new(method, :resources) - end - - if method == RESOURCES_SUBSCRIBE && !capabilities[:resources][:subscribe] - raise MissingRequiredCapabilityError.new(method, :resources_subscribe) - end - when TOOLS_CALL, TOOLS_LIST - unless capabilities[:tools] - raise MissingRequiredCapabilityError.new(method, :tools) - end - when SAMPLING_CREATE_MESSAGE - unless capabilities[:sampling] - raise MissingRequiredCapabilityError.new(method, :sampling) - end - when COMPLETION_COMPLETE - unless capabilities[:completions] - raise MissingRequiredCapabilityError.new(method, :completions) - end - when LOGGING_SET_LEVEL - # Logging is unsupported by the Server - unless capabilities[:logging] - raise MissingRequiredCapabilityError.new(method, :logging) - end - when INITIALIZE, PING - # No specific capability required for initialize or ping end end end From 280e835184c5d2c1885e9f30d1e38cb3cb7f2bde Mon Sep 17 00:00:00 2001 From: Kevin Fischer Date: Sun, 15 Jun 2025 17:26:07 +0900 Subject: [PATCH 4/5] Refactor capability check --- lib/mcp/methods.rb | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/lib/mcp/methods.rb b/lib/mcp/methods.rb index fa7b9d93..226f0111 100644 --- a/lib/mcp/methods.rb +++ b/lib/mcp/methods.rb @@ -47,38 +47,34 @@ class << self def ensure_capability!(method, capabilities) case method when PROMPTS_GET, PROMPTS_LIST - unless capabilities[:prompts] - raise MissingRequiredCapabilityError.new(method, :prompts) - end + require_capability!(method, capabilities, :prompts) when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ, RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE - unless capabilities[:resources] - raise MissingRequiredCapabilityError.new(method, :resources) - end - + require_capability!(method, capabilities, :resources) if method == RESOURCES_SUBSCRIBE && !capabilities[:resources][:subscribe] raise MissingRequiredCapabilityError.new(method, :resources_subscribe) end when TOOLS_CALL, TOOLS_LIST - unless capabilities[:tools] - raise MissingRequiredCapabilityError.new(method, :tools) - end + require_capability!(method, capabilities, :tools) when SAMPLING_CREATE_MESSAGE - unless capabilities[:sampling] - raise MissingRequiredCapabilityError.new(method, :sampling) - end + require_capability!(method, capabilities, :sampling) when COMPLETION_COMPLETE - unless capabilities[:completions] - raise MissingRequiredCapabilityError.new(method, :completions) - end + require_capability!(method, capabilities, :completions) when LOGGING_SET_LEVEL - # Logging is unsupported by the Server - unless capabilities[:logging] - raise MissingRequiredCapabilityError.new(method, :logging) - end + require_capability!(method, capabilities, :logging) when INITIALIZE, PING # No specific capability required for initialize or ping end end + + private + + def require_capability!(method, capabilities, *keys) + name = keys.join(".") # :resources, :subscribe -> "resources.subscribe" + has_capability = capabilities.dig(*keys) + return if has_capability + + raise MissingRequiredCapabilityError.new(method, name) + end end end end From ff92a3d016f69b7ddec06293d661970ada25d668 Mon Sep 17 00:00:00 2001 From: Kevin Fischer Date: Sun, 15 Jun 2025 17:33:30 +0900 Subject: [PATCH 5/5] Add missing checks --- lib/mcp/methods.rb | 35 +++++++++++++++------ test/mcp/methods_test.rb | 67 ++++++++++++++++++++++++++++------------ test/mcp/server_test.rb | 2 +- 3 files changed, 73 insertions(+), 31 deletions(-) diff --git a/lib/mcp/methods.rb b/lib/mcp/methods.rb index 226f0111..f30eaf2e 100644 --- a/lib/mcp/methods.rb +++ b/lib/mcp/methods.rb @@ -23,6 +23,7 @@ module Methods SAMPLING_CREATE_MESSAGE = "sampling/createMessage" # Notification methods + NOTIFICATIONS_INITIALIZED = "notifications/initialized" NOTIFICATIONS_TOOLS_LIST_CHANGED = "notifications/tools/list_changed" NOTIFICATIONS_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed" NOTIFICATIONS_RESOURCES_LIST_CHANGED = "notifications/resources/list_changed" @@ -48,21 +49,35 @@ def ensure_capability!(method, capabilities) case method when PROMPTS_GET, PROMPTS_LIST require_capability!(method, capabilities, :prompts) - when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ, RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE + when NOTIFICATIONS_PROMPTS_LIST_CHANGED + require_capability!(method, capabilities, :prompts) + require_capability!(method, capabilities, :prompts, :listChanged) + when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ + require_capability!(method, capabilities, :resources) + when NOTIFICATIONS_RESOURCES_LIST_CHANGED + require_capability!(method, capabilities, :resources) + require_capability!(method, capabilities, :resources, :listChanged) + when RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE, NOTIFICATIONS_RESOURCES_UPDATED require_capability!(method, capabilities, :resources) - if method == RESOURCES_SUBSCRIBE && !capabilities[:resources][:subscribe] - raise MissingRequiredCapabilityError.new(method, :resources_subscribe) - end + require_capability!(method, capabilities, :resources, :subscribe) when TOOLS_CALL, TOOLS_LIST require_capability!(method, capabilities, :tools) - when SAMPLING_CREATE_MESSAGE - require_capability!(method, capabilities, :sampling) + when NOTIFICATIONS_TOOLS_LIST_CHANGED + require_capability!(method, capabilities, :tools) + require_capability!(method, capabilities, :tools, :listChanged) + when LOGGING_SET_LEVEL, NOTIFICATIONS_MESSAGE + require_capability!(method, capabilities, :logging) when COMPLETION_COMPLETE require_capability!(method, capabilities, :completions) - when LOGGING_SET_LEVEL - require_capability!(method, capabilities, :logging) - when INITIALIZE, PING - # No specific capability required for initialize or ping + when ROOTS_LIST + require_capability!(method, capabilities, :roots) + when NOTIFICATIONS_ROOTS_LIST_CHANGED + require_capability!(method, capabilities, :roots) + require_capability!(method, capabilities, :roots, :listChanged) + when SAMPLING_CREATE_MESSAGE + require_capability!(method, capabilities, :sampling) + when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED + # No specific capability required for initialize, ping, progress or cancelled end end diff --git a/test/mcp/methods_test.rb b/test/mcp/methods_test.rb index 17e99b8e..d89981e3 100644 --- a/test/mcp/methods_test.rb +++ b/test/mcp/methods_test.rb @@ -22,35 +22,62 @@ def ensure_capability_does_not_raise_for(method, capabilities: {}) end end - # Tools capability tests + # Server methods and notifications + ensure_capability_does_not_raise_for Methods::INITIALIZE + + ensure_capability_raises_error_for Methods::PROMPTS_LIST, required_capability_name: "prompts" + ensure_capability_raises_error_for Methods::PROMPTS_GET, required_capability_name: "prompts" + ensure_capability_raises_error_for Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED, required_capability_name: "prompts" + ensure_capability_raises_error_for Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED, + required_capability_name: "prompts.listChanged", + capabilities: { prompts: {} } + + ensure_capability_raises_error_for Methods::RESOURCES_LIST, required_capability_name: "resources" + ensure_capability_raises_error_for Methods::RESOURCES_READ, required_capability_name: "resources" + ensure_capability_raises_error_for Methods::RESOURCES_TEMPLATES_LIST, required_capability_name: "resources" + ensure_capability_raises_error_for Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED, required_capability_name: "resources" + ensure_capability_raises_error_for Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED, + required_capability_name: "resources.listChanged", + capabilities: { resources: {} } + ensure_capability_raises_error_for Methods::RESOURCES_SUBSCRIBE, required_capability_name: "resources" + ensure_capability_raises_error_for Methods::RESOURCES_SUBSCRIBE, + required_capability_name: "resources.subscribe", + capabilities: { resources: {} } + ensure_capability_raises_error_for Methods::RESOURCES_UNSUBSCRIBE, required_capability_name: "resources" + ensure_capability_raises_error_for Methods::RESOURCES_UNSUBSCRIBE, + required_capability_name: "resources.subscribe", + capabilities: { resources: {} } + ensure_capability_raises_error_for Methods::NOTIFICATIONS_RESOURCES_UPDATED, required_capability_name: "resources" + ensure_capability_raises_error_for Methods::NOTIFICATIONS_RESOURCES_UPDATED, + required_capability_name: "resources.subscribe", + capabilities: { resources: {} } + ensure_capability_raises_error_for Methods::TOOLS_LIST, required_capability_name: "tools" ensure_capability_raises_error_for Methods::TOOLS_CALL, required_capability_name: "tools" + ensure_capability_raises_error_for Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED, required_capability_name: "tools" + ensure_capability_raises_error_for Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED, + required_capability_name: "tools.listChanged", + capabilities: { tools: {} } - # Sampling capability tests - ensure_capability_raises_error_for Methods::SAMPLING_CREATE_MESSAGE, required_capability_name: "sampling" + ensure_capability_raises_error_for Methods::LOGGING_SET_LEVEL, required_capability_name: "logging" + ensure_capability_raises_error_for Methods::NOTIFICATIONS_MESSAGE, required_capability_name: "logging" - # Completions capability tests ensure_capability_raises_error_for Methods::COMPLETION_COMPLETE, required_capability_name: "completions" - # Logging capability tests - ensure_capability_raises_error_for Methods::LOGGING_SET_LEVEL, required_capability_name: "logging" + # Client methods and notifications + ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_INITIALIZED - # Prompts capability tests - ensure_capability_raises_error_for Methods::PROMPTS_GET, required_capability_name: "prompts" - ensure_capability_raises_error_for Methods::PROMPTS_LIST, required_capability_name: "prompts" + ensure_capability_raises_error_for Methods::ROOTS_LIST, required_capability_name: "roots" + ensure_capability_raises_error_for Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED, required_capability_name: "roots" + ensure_capability_raises_error_for Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED, + required_capability_name: "roots.listChanged", + capabilities: { roots: {} } - # Resources capability tests - ensure_capability_raises_error_for Methods::RESOURCES_LIST, required_capability_name: "resources" - ensure_capability_raises_error_for Methods::RESOURCES_TEMPLATES_LIST, required_capability_name: "resources" - ensure_capability_raises_error_for Methods::RESOURCES_READ, required_capability_name: "resources" - - # Resources subscribe capability tests - ensure_capability_raises_error_for Methods::RESOURCES_SUBSCRIBE, - required_capability_name: "resources_subscribe", - capabilities: { resources: {} } + ensure_capability_raises_error_for Methods::SAMPLING_CREATE_MESSAGE, required_capability_name: "sampling" - # Methods that don't require capabilities + # Methods and notifications of both server and client ensure_capability_does_not_raise_for Methods::PING - ensure_capability_does_not_raise_for Methods::INITIALIZE + ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_PROGRESS + ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_CANCELLED end end diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index 6dc56ee4..77cc6d37 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -635,7 +635,7 @@ def call(message:, server_context: nil) test "#handle method with missing required nested capability returns an error" do @server.capabilities = { resources: {} } response = @server.handle({ jsonrpc: "2.0", method: "resources/subscribe", id: 1 }) - assert_equal "Server does not support resources_subscribe (required for resources/subscribe)", + assert_equal "Server does not support resources.subscribe (required for resources/subscribe)", response[:error][:data] end