Merge pull request #63 from kfischer-okarin/check-notification-capabilities
Check all methods & capabilities
This commit is contained in:
commit
13bee9f60e
3 changed files with 114 additions and 89 deletions
|
|
@ -19,12 +19,19 @@ module MCP
|
|||
TOOLS_CALL = "tools/call"
|
||||
TOOLS_LIST = "tools/list"
|
||||
|
||||
ROOTS_LIST = "roots/list"
|
||||
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"
|
||||
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
|
||||
|
|
@ -37,41 +44,51 @@ module MCP
|
|||
end
|
||||
end
|
||||
|
||||
extend self
|
||||
class << self
|
||||
def ensure_capability!(method, capabilities)
|
||||
case method
|
||||
when PROMPTS_GET, PROMPTS_LIST
|
||||
require_capability!(method, capabilities, :prompts)
|
||||
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)
|
||||
require_capability!(method, capabilities, :resources, :subscribe)
|
||||
when TOOLS_CALL, TOOLS_LIST
|
||||
require_capability!(method, capabilities, :tools)
|
||||
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 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
|
||||
|
||||
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
|
||||
private
|
||||
|
||||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -5,71 +5,79 @@ require "test_helper"
|
|||
|
||||
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, {})
|
||||
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, {})
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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, {})
|
||||
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
|
||||
assert_equal "Server does not support prompts (required for #{method})", error.message
|
||||
end
|
||||
end
|
||||
|
||||
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, {})
|
||||
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
|
||||
assert_equal "Server does not support resources (required for #{method})", error.message
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
# Server methods and notifications
|
||||
ensure_capability_does_not_raise_for Methods::INITIALIZE
|
||||
|
||||
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
|
||||
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: {} }
|
||||
|
||||
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
|
||||
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: {} }
|
||||
|
||||
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"
|
||||
|
||||
ensure_capability_raises_error_for Methods::COMPLETION_COMPLETE, required_capability_name: "completions"
|
||||
|
||||
# Client methods and notifications
|
||||
ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_INITIALIZED
|
||||
|
||||
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: {} }
|
||||
|
||||
ensure_capability_raises_error_for Methods::SAMPLING_CREATE_MESSAGE, required_capability_name: "sampling"
|
||||
|
||||
# Methods and notifications of both server and client
|
||||
ensure_capability_does_not_raise_for Methods::PING
|
||||
ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_PROGRESS
|
||||
ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_CANCELLED
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -635,7 +635,7 @@ module MCP
|
|||
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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue