Fix JSON-RPC notification format in Streamable HTTP transport

Update send_notification method to properly construct JSON-RPC notification objects with jsonrpc version and method fields.
This ensures notifications conform to the JSON-RPC 2.0 specification when sent over HTTP transport.

https://www.jsonrpc.org/specification#notification

Another goal is to standardise the `send_notification` interface.
Before the revision, it was sufficient to pass the method and parameters in the case of stdio.
However, in the case of Streamable, a hash based on the JSON-RPC specification must be passed.
This commit is contained in:
dak2 2025-07-26 17:57:51 +09:00
commit e7fafddf94
3 changed files with 17 additions and 14 deletions

View file

@ -34,7 +34,13 @@ module MCP
end
end
def send_notification(notification, session_id: nil)
def send_notification(method, params = nil, session_id: nil)
notification = {
jsonrpc: "2.0",
method:,
}
notification[:params] = params if params
@mutex.synchronize do
if session_id
# Send to specific session

View file

@ -58,9 +58,9 @@ module MCP
io.rewind
output = io.read
assert_includes output, "data: #{Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED}"
assert_includes output, "data: #{Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED}"
assert_includes output, "data: #{Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED}"
assert_includes output, "data: {\"jsonrpc\":\"2.0\",\"method\":\"#{Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED}\"}"
assert_includes output, "data: {\"jsonrpc\":\"2.0\",\"method\":\"#{Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED}\"}"
assert_includes output, "data: {\"jsonrpc\":\"2.0\",\"method\":\"#{Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED}\"}"
end
test "notifications are broadcast to all connected sessions" do
@ -111,11 +111,11 @@ module MCP
# Check both sessions received the notification
io1.rewind
output1 = io1.read
assert_includes output1, "data: #{Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED}"
assert_includes output1, "data: {\"jsonrpc\":\"2.0\",\"method\":\"#{Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED}\"}"
io2.rewind
output2 = io2.read
assert_includes output2, "data: #{Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED}"
assert_includes output2, "data: {\"jsonrpc\":\"2.0\",\"method\":\"#{Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED}\"}"
end
test "server continues to work when SSE connection is closed" do
@ -191,7 +191,7 @@ module MCP
# Check the notification was received
io.rewind
output = io.read
assert_includes output, "data: #{Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED}"
assert_includes output, "data: {\"jsonrpc\":\"2.0\",\"method\":\"#{Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED}\"}"
# Verify the tool was added to the server
assert @server.tools.key?("dynamic_tool")
@ -229,7 +229,7 @@ module MCP
output = io.read
# SSE format should be "data: <message>\n\n"
assert_match(/data: #{Regexp.escape(Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED)}\n/, output)
assert_match(/data: \{"jsonrpc":"2\.0","method":"#{Regexp.escape(Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED)}"\}\n/, output)
end
private

View file

@ -391,8 +391,7 @@ module MCP
@server.define_singleton_method(:handle_json) do |request|
result = original_handle_json.call(request)
# Send notification while still in request context - broadcast to all sessions
notification = { jsonrpc: "2.0", method: "test_notification", params: { session: "current" } }
transport.send_notification(notification)
transport.send_notification("test_notification", { session: "current" })
result
end
@ -452,8 +451,7 @@ module MCP
sleep(0.1)
# Send notification to specific session
notification = { jsonrpc: "2.0", method: "test_notification", params: { message: "Hello" } }
result = @transport.send_notification(notification, session_id: session_id)
result = @transport.send_notification("test_notification", { message: "Hello" }, session_id: session_id)
assert result
@ -507,8 +505,7 @@ module MCP
sleep(0.1)
# Broadcast notification to all sessions
notification = { jsonrpc: "2.0", method: "broadcast", params: { message: "Hello everyone" } }
sent_count = @transport.send_notification(notification)
sent_count = @transport.send_notification("broadcast", { message: "Hello everyone" })
assert_equal 2, sent_count