Skip to content

Commit 13bee9f

Browse files
authored
Merge pull request #63 from kfischer-okarin/check-notification-capabilities
Check all methods & capabilities
2 parents 1a89cc5 + ff92a3d commit 13bee9f

File tree

3 files changed

+114
-89
lines changed

3 files changed

+114
-89
lines changed

lib/mcp/methods.rb

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,19 @@ module Methods
1919
TOOLS_CALL = "tools/call"
2020
TOOLS_LIST = "tools/list"
2121

22+
ROOTS_LIST = "roots/list"
2223
SAMPLING_CREATE_MESSAGE = "sampling/createMessage"
2324

2425
# Notification methods
26+
NOTIFICATIONS_INITIALIZED = "notifications/initialized"
2527
NOTIFICATIONS_TOOLS_LIST_CHANGED = "notifications/tools/list_changed"
2628
NOTIFICATIONS_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed"
2729
NOTIFICATIONS_RESOURCES_LIST_CHANGED = "notifications/resources/list_changed"
30+
NOTIFICATIONS_RESOURCES_UPDATED = "notifications/resources/updated"
31+
NOTIFICATIONS_ROOTS_LIST_CHANGED = "notifications/roots/list_changed"
32+
NOTIFICATIONS_MESSAGE = "notifications/message"
33+
NOTIFICATIONS_PROGRESS = "notifications/progress"
34+
NOTIFICATIONS_CANCELLED = "notifications/cancelled"
2835

2936
class MissingRequiredCapabilityError < StandardError
3037
attr_reader :method
@@ -37,41 +44,51 @@ def initialize(method, capability)
3744
end
3845
end
3946

40-
extend self
41-
42-
def ensure_capability!(method, capabilities)
43-
case method
44-
when PROMPTS_GET, PROMPTS_LIST
45-
unless capabilities[:prompts]
46-
raise MissingRequiredCapabilityError.new(method, :prompts)
47-
end
48-
when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ, RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE
49-
unless capabilities[:resources]
50-
raise MissingRequiredCapabilityError.new(method, :resources)
47+
class << self
48+
def ensure_capability!(method, capabilities)
49+
case method
50+
when PROMPTS_GET, PROMPTS_LIST
51+
require_capability!(method, capabilities, :prompts)
52+
when NOTIFICATIONS_PROMPTS_LIST_CHANGED
53+
require_capability!(method, capabilities, :prompts)
54+
require_capability!(method, capabilities, :prompts, :listChanged)
55+
when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ
56+
require_capability!(method, capabilities, :resources)
57+
when NOTIFICATIONS_RESOURCES_LIST_CHANGED
58+
require_capability!(method, capabilities, :resources)
59+
require_capability!(method, capabilities, :resources, :listChanged)
60+
when RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE, NOTIFICATIONS_RESOURCES_UPDATED
61+
require_capability!(method, capabilities, :resources)
62+
require_capability!(method, capabilities, :resources, :subscribe)
63+
when TOOLS_CALL, TOOLS_LIST
64+
require_capability!(method, capabilities, :tools)
65+
when NOTIFICATIONS_TOOLS_LIST_CHANGED
66+
require_capability!(method, capabilities, :tools)
67+
require_capability!(method, capabilities, :tools, :listChanged)
68+
when LOGGING_SET_LEVEL, NOTIFICATIONS_MESSAGE
69+
require_capability!(method, capabilities, :logging)
70+
when COMPLETION_COMPLETE
71+
require_capability!(method, capabilities, :completions)
72+
when ROOTS_LIST
73+
require_capability!(method, capabilities, :roots)
74+
when NOTIFICATIONS_ROOTS_LIST_CHANGED
75+
require_capability!(method, capabilities, :roots)
76+
require_capability!(method, capabilities, :roots, :listChanged)
77+
when SAMPLING_CREATE_MESSAGE
78+
require_capability!(method, capabilities, :sampling)
79+
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED
80+
# No specific capability required for initialize, ping, progress or cancelled
5181
end
82+
end
5283

53-
if method == RESOURCES_SUBSCRIBE && !capabilities[:resources][:subscribe]
54-
raise MissingRequiredCapabilityError.new(method, :resources_subscribe)
55-
end
56-
when TOOLS_CALL, TOOLS_LIST
57-
unless capabilities[:tools]
58-
raise MissingRequiredCapabilityError.new(method, :tools)
59-
end
60-
when SAMPLING_CREATE_MESSAGE
61-
unless capabilities[:sampling]
62-
raise MissingRequiredCapabilityError.new(method, :sampling)
63-
end
64-
when COMPLETION_COMPLETE
65-
unless capabilities[:completions]
66-
raise MissingRequiredCapabilityError.new(method, :completions)
67-
end
68-
when LOGGING_SET_LEVEL
69-
# Logging is unsupported by the Server
70-
unless capabilities[:logging]
71-
raise MissingRequiredCapabilityError.new(method, :logging)
72-
end
73-
when INITIALIZE, PING
74-
# No specific capability required for initialize or ping
84+
private
85+
86+
def require_capability!(method, capabilities, *keys)
87+
name = keys.join(".") # :resources, :subscribe -> "resources.subscribe"
88+
has_capability = capabilities.dig(*keys)
89+
return if has_capability
90+
91+
raise MissingRequiredCapabilityError.new(method, name)
7592
end
7693
end
7794
end

test/mcp/methods_test.rb

Lines changed: 63 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,71 +5,79 @@
55

66
module MCP
77
class MethodsTest < ActiveSupport::TestCase
8-
test "ensure_capability! for tools/list method raises an error if tools capability is not present" do
9-
error = assert_raises(Methods::MissingRequiredCapabilityError) do
10-
Methods.ensure_capability!(Methods::TOOLS_LIST, {})
8+
class << self
9+
def ensure_capability_raises_error_for(method, required_capability_name:, capabilities: {})
10+
test("ensure_capability! for #{method} raises an error if #{required_capability_name} capability is not present") do
11+
error = assert_raises(Methods::MissingRequiredCapabilityError) do
12+
Methods.ensure_capability!(method, capabilities)
13+
end
14+
assert_equal("Server does not support #{required_capability_name} (required for #{method})", error.message)
15+
end
1116
end
12-
assert_equal "Server does not support tools (required for tools/list)", error.message
13-
end
1417

15-
test "ensure_capability! for sampling/createMessage raises an error if sampling capability is not present" do
16-
error = assert_raises(Methods::MissingRequiredCapabilityError) do
17-
Methods.ensure_capability!(Methods::SAMPLING_CREATE_MESSAGE, {})
18+
def ensure_capability_does_not_raise_for(method, capabilities: {})
19+
test("ensure_capability! does not raise for #{method}") do
20+
assert_nothing_raised { Methods.ensure_capability!(method, capabilities) }
21+
end
1822
end
19-
assert_equal "Server does not support sampling (required for sampling/createMessage)", error.message
2023
end
2124

22-
test "ensure_capability! for completion/complete raises an error if completions capability is not present" do
23-
error = assert_raises(Methods::MissingRequiredCapabilityError) do
24-
Methods.ensure_capability!(Methods::COMPLETION_COMPLETE, {})
25-
end
26-
assert_equal "Server does not support completions (required for completion/complete)", error.message
27-
end
25+
# Server methods and notifications
26+
ensure_capability_does_not_raise_for Methods::INITIALIZE
2827

29-
test "ensure_capability! for logging/setLevel raises an error if logging capability is not present" do
30-
error = assert_raises(Methods::MissingRequiredCapabilityError) do
31-
Methods.ensure_capability!(Methods::LOGGING_SET_LEVEL, {})
32-
end
33-
assert_equal "Server does not support logging (required for logging/setLevel)", error.message
34-
end
28+
ensure_capability_raises_error_for Methods::PROMPTS_LIST, required_capability_name: "prompts"
29+
ensure_capability_raises_error_for Methods::PROMPTS_GET, required_capability_name: "prompts"
30+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED, required_capability_name: "prompts"
31+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED,
32+
required_capability_name: "prompts.listChanged",
33+
capabilities: { prompts: {} }
3534

36-
test "ensure_capability! for prompts/get and prompts/list raise an error if prompts capability is not present" do
37-
[Methods::PROMPTS_GET, Methods::PROMPTS_LIST].each do |method|
38-
error = assert_raises(Methods::MissingRequiredCapabilityError) do
39-
Methods.ensure_capability!(method, {})
40-
end
41-
assert_equal "Server does not support prompts (required for #{method})", error.message
42-
end
43-
end
35+
ensure_capability_raises_error_for Methods::RESOURCES_LIST, required_capability_name: "resources"
36+
ensure_capability_raises_error_for Methods::RESOURCES_READ, required_capability_name: "resources"
37+
ensure_capability_raises_error_for Methods::RESOURCES_TEMPLATES_LIST, required_capability_name: "resources"
38+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED, required_capability_name: "resources"
39+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED,
40+
required_capability_name: "resources.listChanged",
41+
capabilities: { resources: {} }
42+
ensure_capability_raises_error_for Methods::RESOURCES_SUBSCRIBE, required_capability_name: "resources"
43+
ensure_capability_raises_error_for Methods::RESOURCES_SUBSCRIBE,
44+
required_capability_name: "resources.subscribe",
45+
capabilities: { resources: {} }
46+
ensure_capability_raises_error_for Methods::RESOURCES_UNSUBSCRIBE, required_capability_name: "resources"
47+
ensure_capability_raises_error_for Methods::RESOURCES_UNSUBSCRIBE,
48+
required_capability_name: "resources.subscribe",
49+
capabilities: { resources: {} }
50+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_RESOURCES_UPDATED, required_capability_name: "resources"
51+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_RESOURCES_UPDATED,
52+
required_capability_name: "resources.subscribe",
53+
capabilities: { resources: {} }
4454

45-
test "ensure_capability! for resources/list, resources/templates/list, resources/read raise an error if resources capability is not present" do
46-
[Methods::RESOURCES_LIST, Methods::RESOURCES_TEMPLATES_LIST, Methods::RESOURCES_READ].each do |method|
47-
error = assert_raises(Methods::MissingRequiredCapabilityError) do
48-
Methods.ensure_capability!(method, {})
49-
end
50-
assert_equal "Server does not support resources (required for #{method})", error.message
51-
end
52-
end
55+
ensure_capability_raises_error_for Methods::TOOLS_LIST, required_capability_name: "tools"
56+
ensure_capability_raises_error_for Methods::TOOLS_CALL, required_capability_name: "tools"
57+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED, required_capability_name: "tools"
58+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED,
59+
required_capability_name: "tools.listChanged",
60+
capabilities: { tools: {} }
5361

54-
test "ensure_capability! for tools/call and tools/list raise an error if tools capability is not present" do
55-
[Methods::TOOLS_CALL, Methods::TOOLS_LIST].each do |method|
56-
error = assert_raises(Methods::MissingRequiredCapabilityError) do
57-
Methods.ensure_capability!(method, {})
58-
end
59-
assert_equal "Server does not support tools (required for #{method})", error.message
60-
end
61-
end
62+
ensure_capability_raises_error_for Methods::LOGGING_SET_LEVEL, required_capability_name: "logging"
63+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_MESSAGE, required_capability_name: "logging"
6264

63-
test "ensure_capability! for resources/subscribe raises an error if resources subscribe capability is not present" do
64-
error = assert_raises(Methods::MissingRequiredCapabilityError) do
65-
Methods.ensure_capability!(Methods::RESOURCES_SUBSCRIBE, { resources: {} })
66-
end
67-
assert_equal "Server does not support resources_subscribe (required for resources/subscribe)", error.message
68-
end
65+
ensure_capability_raises_error_for Methods::COMPLETION_COMPLETE, required_capability_name: "completions"
6966

70-
test "ensure_capability! does not raise for ping and initialize methods" do
71-
assert_nothing_raised { Methods.ensure_capability!(Methods::PING, {}) }
72-
assert_nothing_raised { Methods.ensure_capability!(Methods::INITIALIZE, {}) }
73-
end
67+
# Client methods and notifications
68+
ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_INITIALIZED
69+
70+
ensure_capability_raises_error_for Methods::ROOTS_LIST, required_capability_name: "roots"
71+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED, required_capability_name: "roots"
72+
ensure_capability_raises_error_for Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED,
73+
required_capability_name: "roots.listChanged",
74+
capabilities: { roots: {} }
75+
76+
ensure_capability_raises_error_for Methods::SAMPLING_CREATE_MESSAGE, required_capability_name: "sampling"
77+
78+
# Methods and notifications of both server and client
79+
ensure_capability_does_not_raise_for Methods::PING
80+
ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_PROGRESS
81+
ensure_capability_does_not_raise_for Methods::NOTIFICATIONS_CANCELLED
7482
end
7583
end

test/mcp/server_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ def call(message:, server_context: nil)
635635
test "#handle method with missing required nested capability returns an error" do
636636
@server.capabilities = { resources: {} }
637637
response = @server.handle({ jsonrpc: "2.0", method: "resources/subscribe", id: 1 })
638-
assert_equal "Server does not support resources_subscribe (required for resources/subscribe)",
638+
assert_equal "Server does not support resources.subscribe (required for resources/subscribe)",
639639
response[:error][:data]
640640
end
641641

0 commit comments

Comments
 (0)