There are some unused block variables. This change removes block parameters that consist solely of unused block parameters. Where `Hash#values` suffices instead of ignoring unused variables, replace such code with `Hash#values`.
247 lines
7.7 KiB
Ruby
247 lines
7.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "test_helper"
|
|
|
|
module MCP
|
|
class Server
|
|
module Transports
|
|
class StdioNotificationIntegrationTest < ActiveSupport::TestCase
|
|
class MockIO
|
|
attr_reader :output
|
|
|
|
def initialize
|
|
@output = []
|
|
@closed = false
|
|
end
|
|
|
|
def puts(message)
|
|
@output << message
|
|
end
|
|
|
|
def write(message)
|
|
@output << message
|
|
message.length
|
|
end
|
|
|
|
def gets
|
|
nil # Simulate end of input
|
|
end
|
|
|
|
def set_encoding(encoding) # rubocop:disable Naming/AccessorMethodName
|
|
# Mock implementation
|
|
end
|
|
|
|
def flush
|
|
# Mock implementation
|
|
end
|
|
|
|
def close
|
|
@closed = true
|
|
end
|
|
|
|
def closed?
|
|
@closed
|
|
end
|
|
end
|
|
|
|
setup do
|
|
@original_stdout = $stdout
|
|
@original_stdin = $stdin
|
|
|
|
@mock_stdout = MockIO.new
|
|
@mock_stdin = MockIO.new
|
|
|
|
$stdout = @mock_stdout
|
|
$stdin = @mock_stdin
|
|
|
|
@server = Server.new(
|
|
name: "test_server",
|
|
tools: [],
|
|
prompts: [],
|
|
resources: [],
|
|
)
|
|
@transport = StdioTransport.new(@server)
|
|
@server.transport = @transport
|
|
end
|
|
|
|
teardown do
|
|
$stdout = @original_stdout
|
|
$stdin = @original_stdin
|
|
end
|
|
|
|
test "server notification methods send JSON-RPC notifications through StdioTransport" do
|
|
# Test tools notification
|
|
@server.notify_tools_list_changed
|
|
|
|
# Test prompts notification
|
|
@server.notify_prompts_list_changed
|
|
|
|
# Test resources notification
|
|
@server.notify_resources_list_changed
|
|
|
|
# Check the notifications were sent
|
|
assert_equal 3, @mock_stdout.output.size
|
|
|
|
# Parse and verify each notification
|
|
notifications = @mock_stdout.output.map { |msg| JSON.parse(msg) }
|
|
|
|
assert_equal "2.0", notifications[0]["jsonrpc"]
|
|
assert_equal Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED, notifications[0]["method"]
|
|
assert_nil notifications[0]["params"]
|
|
|
|
assert_equal "2.0", notifications[1]["jsonrpc"]
|
|
assert_equal Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED, notifications[1]["method"]
|
|
assert_nil notifications[1]["params"]
|
|
|
|
assert_equal "2.0", notifications[2]["jsonrpc"]
|
|
assert_equal Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED, notifications[2]["method"]
|
|
assert_nil notifications[2]["params"]
|
|
end
|
|
|
|
test "notifications include params when provided" do
|
|
# Test the transport's send_notification directly with params
|
|
result = @transport.send_notification("test/notification", { data: "test_value" })
|
|
|
|
assert result
|
|
assert_equal 1, @mock_stdout.output.size
|
|
|
|
notification = JSON.parse(@mock_stdout.output.first)
|
|
assert_equal "2.0", notification["jsonrpc"]
|
|
assert_equal "test/notification", notification["method"]
|
|
assert_equal({ "data" => "test_value" }, notification["params"])
|
|
end
|
|
|
|
test "server continues to work when stdout is closed" do
|
|
# Close stdout
|
|
@mock_stdout.close
|
|
|
|
# Server notifications should not raise errors
|
|
assert_nothing_raised do
|
|
@server.notify_tools_list_changed
|
|
@server.notify_prompts_list_changed
|
|
@server.notify_resources_list_changed
|
|
end
|
|
end
|
|
|
|
test "notifications work with dynamic tool additions" do
|
|
# Define a new tool
|
|
@server.define_tool(
|
|
name: "dynamic_tool",
|
|
description: "A dynamically added tool",
|
|
) do |**_args|
|
|
{ result: "success" }
|
|
end
|
|
|
|
# Clear previous output
|
|
@mock_stdout.output.clear
|
|
|
|
# Manually trigger notification
|
|
@server.notify_tools_list_changed
|
|
|
|
# Check the notification was sent
|
|
assert_equal 1, @mock_stdout.output.size
|
|
|
|
notification = JSON.parse(@mock_stdout.output.first)
|
|
assert_equal Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED, notification["method"]
|
|
|
|
# Verify the tool was added to the server
|
|
assert @server.tools.key?("dynamic_tool")
|
|
end
|
|
|
|
test "notifications are properly formatted JSON-RPC 2.0 messages" do
|
|
# Send a notification
|
|
@server.notify_prompts_list_changed
|
|
|
|
# Verify format
|
|
assert_equal 1, @mock_stdout.output.size
|
|
output = @mock_stdout.output.first
|
|
|
|
# Should be valid JSON
|
|
notification = JSON.parse(output)
|
|
|
|
# Should have required JSON-RPC 2.0 fields
|
|
assert_equal "2.0", notification["jsonrpc"]
|
|
assert notification.key?("method")
|
|
refute notification.key?("id") # Notifications should not have an id
|
|
|
|
# Method should be the expected notification type
|
|
assert_equal Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED, notification["method"]
|
|
end
|
|
|
|
test "multiple notifications are sent as separate JSON messages" do
|
|
# Send multiple notifications rapidly
|
|
5.times do
|
|
@server.notify_tools_list_changed
|
|
end
|
|
|
|
# Each should be a separate JSON message
|
|
assert_equal 5, @mock_stdout.output.size
|
|
|
|
# All should be parseable as JSON
|
|
@mock_stdout.output.each do |msg|
|
|
notification = JSON.parse(msg)
|
|
assert_equal "2.0", notification["jsonrpc"]
|
|
assert_equal Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED, notification["method"]
|
|
end
|
|
end
|
|
|
|
test "transport handles errors gracefully" do
|
|
# Create a stdout that raises errors
|
|
error_stdout = Class.new(MockIO) do
|
|
def puts(message)
|
|
raise IOError, "Simulated IO error"
|
|
end
|
|
end.new
|
|
|
|
$stdout = error_stdout
|
|
|
|
# Notification should return false but not raise
|
|
result = @transport.send_notification("test/notification")
|
|
refute result
|
|
end
|
|
|
|
test "server notification flow works end-to-end with StdioTransport" do
|
|
# This test verifies the complete integration from server to transport
|
|
|
|
# Start with no output
|
|
assert_empty @mock_stdout.output
|
|
|
|
# Add a prompt and notify
|
|
@server.define_prompt(
|
|
name: "test_prompt",
|
|
description: "Test prompt",
|
|
) do
|
|
MCP::PromptResponse.new(messages: [{ role: "user", content: "Test" }])
|
|
end
|
|
|
|
# Manually trigger notification
|
|
@server.notify_prompts_list_changed
|
|
|
|
# Verify notification was sent
|
|
assert_equal 1, @mock_stdout.output.size
|
|
notification = JSON.parse(@mock_stdout.output.first)
|
|
assert_equal Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED, notification["method"]
|
|
|
|
# Add a resource and notify
|
|
@server.resources = [
|
|
MCP::Resource.new(
|
|
uri: "https://test_resource.invalid",
|
|
name: "test-resource",
|
|
title: "Test Resource",
|
|
description: "A test resource",
|
|
mime_type: "text/plain",
|
|
),
|
|
]
|
|
|
|
# Manually trigger notification
|
|
@server.notify_resources_list_changed
|
|
|
|
# Verify both notifications were sent
|
|
assert_equal 2, @mock_stdout.output.size
|
|
second_notification = JSON.parse(@mock_stdout.output.last)
|
|
assert_equal Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED, second_notification["method"]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|