Skip to content

Commit fb2b06b

Browse files
Add notification support
Co-authored-by: Kevin Fischer <[email protected]>
1 parent 44a35b2 commit fb2b06b

File tree

7 files changed

+709
-2
lines changed

7 files changed

+709
-2
lines changed

README.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ It implements the Model Context Protocol specification, handling model context r
3434
- Supports prompt registration and execution
3535
- Supports resource registration and retrieval
3636
- Supports stdio & Streamable HTTP (including SSE) transports
37+
- Supports notifications for list changes (tools, prompts, resources)
3738

3839
### Supported Methods
3940
- `initialize` - Initializes the protocol and returns server capabilities
@@ -46,13 +47,46 @@ It implements the Model Context Protocol specification, handling model context r
4647
- `resources/read` - Retrieves a specific resource by name
4748
- `resources/templates/list` - Lists all registered resource templates and their schemas
4849

50+
### Notifications
51+
52+
The server supports sending notifications to clients when lists of tools, prompts, or resources change. This enables real-time updates without polling.
53+
54+
#### Notification Methods
55+
56+
The server provides three notification methods:
57+
- `notify_tools_list_changed()` - Send a notification when the tools list changes
58+
- `notify_prompts_list_changed()` - Send a notification when the prompts list changes
59+
- `notify_resources_list_changed()` - Send a notification when the resources list changes
60+
61+
#### Notification Format
62+
63+
Notifications follow the JSON-RPC 2.0 specification and use these method names:
64+
- `notifications/tools/list_changed`
65+
- `notifications/prompts/list_changed`
66+
- `notifications/resources/list_changed`
67+
68+
#### Transport Support
69+
70+
- **HTTP Transport**: Notifications are sent as Server-Sent Events (SSE) to all connected sessions
71+
- **Stdio Transport**: Notifications are sent as JSON-RPC 2.0 messages to stdout
72+
73+
#### Usage Example
74+
75+
```ruby
76+
server = MCP::Server.new(name: "my_server")
77+
transport = MCP::Transports::HTTP.new(server)
78+
server.transport = transport
79+
80+
# When tools change, notify clients
81+
server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
82+
server.notify_tools_list_changed()
83+
```
84+
4985
### Unsupported Features ( to be implemented in future versions )
5086

51-
- Notifications
5287
- Log Level
5388
- Resource subscriptions
5489
- Completions
55-
- Complete Streamable HTTP implementation with streaming responses
5690

5791
### Usage
5892

lib/mcp/methods.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ module Methods
2121

2222
SAMPLING_CREATE_MESSAGE = "sampling/createMessage"
2323

24+
# Notification methods
25+
NOTIFICATIONS_TOOLS_LIST_CHANGED = "notifications/tools/list_changed"
26+
NOTIFICATIONS_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed"
27+
NOTIFICATIONS_RESOURCES_LIST_CHANGED = "notifications/resources/list_changed"
28+
2429
class MissingRequiredCapabilityError < StandardError
2530
attr_reader :method
2631
attr_reader :capability

lib/mcp/server.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,30 @@ def define_prompt(name: nil, description: nil, arguments: [], &block)
9393
@prompts[prompt.name_value] = prompt
9494
end
9595

96+
def notify_tools_list_changed
97+
return unless @transport
98+
99+
@transport.send_notification(Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED)
100+
rescue => e
101+
report_exception(e, { notification: "tools_list_changed" })
102+
end
103+
104+
def notify_prompts_list_changed
105+
return unless @transport
106+
107+
@transport.send_notification(Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED)
108+
rescue => e
109+
report_exception(e, { notification: "prompts_list_changed" })
110+
end
111+
112+
def notify_resources_list_changed
113+
return unless @transport
114+
115+
@transport.send_notification(Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED)
116+
rescue => e
117+
report_exception(e, { notification: "resources_list_changed" })
118+
end
119+
96120
def resources_list_handler(&block)
97121
@handlers[Methods::RESOURCES_LIST] = block
98122
end

lib/mcp/transports/stdio.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ def send_response(message)
3636
$stdout.puts(json_message)
3737
$stdout.flush
3838
end
39+
40+
def send_notification(method, params = nil)
41+
notification = {
42+
jsonrpc: "2.0",
43+
method: method
44+
}
45+
notification[:params] = params if params
46+
47+
send_response(notification)
48+
true
49+
rescue => e
50+
MCP.configuration.exception_reporter.call(e, { error: "Failed to send notification" })
51+
false
52+
end
3953
end
4054
end
4155
end

test/mcp/server_notification_test.rb

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
require "test_helper"
5+
6+
module MCP
7+
class ServerNotificationTest < ActiveSupport::TestCase
8+
include InstrumentationTestHelper
9+
10+
class MockTransport < Transport
11+
attr_reader :notifications
12+
13+
def initialize(server)
14+
super
15+
@notifications = []
16+
end
17+
18+
def send_notification(method, params = nil)
19+
@notifications << { method: method, params: params }
20+
true
21+
end
22+
23+
def send_response(response); end
24+
def open; end
25+
def close; end
26+
def handle_request(request); end
27+
end
28+
29+
setup do
30+
configuration = MCP::Configuration.new
31+
configuration.instrumentation_callback = instrumentation_helper.callback
32+
33+
@server = Server.new(
34+
name: "test_server",
35+
version: "1.0.0",
36+
configuration: configuration
37+
)
38+
39+
@mock_transport = MockTransport.new(@server)
40+
@server.transport = @mock_transport
41+
end
42+
43+
test "#notify_tools_list_changed sends notification through transport" do
44+
@server.notify_tools_list_changed
45+
46+
assert_equal 1, @mock_transport.notifications.size
47+
notification = @mock_transport.notifications.first
48+
assert_equal Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED, notification[:method]
49+
assert_nil notification[:params]
50+
end
51+
52+
test "#notify_prompts_list_changed sends notification through transport" do
53+
@server.notify_prompts_list_changed
54+
55+
assert_equal 1, @mock_transport.notifications.size
56+
notification = @mock_transport.notifications.first
57+
assert_equal Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED, notification[:method]
58+
assert_nil notification[:params]
59+
end
60+
61+
test "#notify_resources_list_changed sends notification through transport" do
62+
@server.notify_resources_list_changed
63+
64+
assert_equal 1, @mock_transport.notifications.size
65+
notification = @mock_transport.notifications.first
66+
assert_equal Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED, notification[:method]
67+
assert_nil notification[:params]
68+
end
69+
70+
test "notification methods work without transport" do
71+
server_without_transport = Server.new(name: "test_server")
72+
73+
# Should not raise any errors
74+
assert_nothing_raised do
75+
server_without_transport.notify_tools_list_changed
76+
server_without_transport.notify_prompts_list_changed
77+
server_without_transport.notify_resources_list_changed
78+
end
79+
end
80+
81+
test "notification methods handle transport errors gracefully" do
82+
# Create a transport that raises errors
83+
error_transport = Class.new(MockTransport) do
84+
def send_notification(method, params = nil)
85+
raise StandardError, "Transport error"
86+
end
87+
end.new(@server)
88+
89+
@server.transport = error_transport
90+
91+
# Mock the exception reporter
92+
expected_contexts = [
93+
{ notification: "tools_list_changed" },
94+
{ notification: "prompts_list_changed" },
95+
{ notification: "resources_list_changed" }
96+
]
97+
98+
call_count = 0
99+
@server.configuration.exception_reporter.expects(:call).times(3).with do |exception, context|
100+
assert_kind_of StandardError, exception
101+
assert_equal "Transport error", exception.message
102+
assert_includes expected_contexts, context
103+
call_count += 1
104+
true
105+
end
106+
107+
# Should not raise errors to the caller
108+
assert_nothing_raised do
109+
@server.notify_tools_list_changed
110+
@server.notify_prompts_list_changed
111+
@server.notify_resources_list_changed
112+
end
113+
114+
assert_equal 3, call_count
115+
end
116+
117+
test "multiple notification methods can be called in sequence" do
118+
@server.notify_tools_list_changed
119+
@server.notify_prompts_list_changed
120+
@server.notify_resources_list_changed
121+
122+
assert_equal 3, @mock_transport.notifications.size
123+
124+
notifications = @mock_transport.notifications
125+
assert_equal Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED, notifications[0][:method]
126+
assert_equal Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED, notifications[1][:method]
127+
assert_equal Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED, notifications[2][:method]
128+
end
129+
130+
test "notification method constants are defined correctly" do
131+
assert_equal "notifications/tools/list_changed", Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED
132+
assert_equal "notifications/prompts/list_changed", Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED
133+
assert_equal "notifications/resources/list_changed", Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED
134+
end
135+
end
136+
end

0 commit comments

Comments
 (0)