|
9 | 9 | module MCP |
10 | 10 | class Client |
11 | 11 | class StdioTest < Minitest::Test |
| 12 | + IMPLICIT_CONNECT_DEPRECATION_WARNING = |
| 13 | + /Calling `MCP::Client::Stdio#send_request` without calling `MCP::Client#connect` is deprecated\. Use `MCP::Client#connect` before sending requests instead\./.freeze |
| 14 | + |
12 | 15 | def test_send_request_starts_process_and_returns_response |
13 | 16 | stdin_read, stdin_write = IO.pipe |
14 | 17 | stdout_read, stdout_write = IO.pipe |
@@ -56,7 +59,10 @@ def test_send_request_starts_process_and_returns_response |
56 | 59 | stdout_write.flush |
57 | 60 | end |
58 | 61 |
|
59 | | - response = transport.send_request(request: request) |
| 62 | + response = nil |
| 63 | + assert_implicit_connect_deprecation_warning do |
| 64 | + response = transport.send_request(request: request) |
| 65 | + end |
60 | 66 |
|
61 | 67 | assert_equal("test-id", response["id"]) |
62 | 68 | assert_equal(1, response.dig("result", "tools").size) |
@@ -123,7 +129,9 @@ def test_send_request_initializes_session_on_first_call |
123 | 129 | stdout_write.flush |
124 | 130 | end |
125 | 131 |
|
126 | | - transport.send_request(request: request) |
| 132 | + assert_implicit_connect_deprecation_warning do |
| 133 | + transport.send_request(request: request) |
| 134 | + end |
127 | 135 |
|
128 | 136 | assert_equal(["initialize", "notifications/initialized", "tools/list"], received_methods) |
129 | 137 | ensure |
@@ -184,10 +192,13 @@ def test_send_request_skips_notifications |
184 | 192 | stdout_write.flush |
185 | 193 | end |
186 | 194 |
|
187 | | - response = transport.send_request(request: request) |
| 195 | + response = nil |
| 196 | + assert_implicit_connect_deprecation_warning do |
| 197 | + response = transport.send_request(request: request) |
| 198 | + end |
188 | 199 |
|
189 | 200 | assert_equal("test-id", response["id"]) |
190 | | - assert_equal([], response.dig("result", "tools")) |
| 201 | + assert_empty(response.dig("result", "tools")) |
191 | 202 | ensure |
192 | 203 | server_thread.join |
193 | 204 | stdin_read.close |
@@ -217,8 +228,11 @@ def test_send_request_raises_error_when_process_exits |
217 | 228 | method: "tools/list", |
218 | 229 | } |
219 | 230 |
|
220 | | - error = assert_raises(RequestHandlerError) do |
221 | | - transport.send_request(request: request) |
| 231 | + error = nil |
| 232 | + assert_implicit_connect_deprecation_warning do |
| 233 | + error = assert_raises(RequestHandlerError) do |
| 234 | + transport.send_request(request: request) |
| 235 | + end |
222 | 236 | end |
223 | 237 |
|
224 | 238 | assert_equal("Server process has exited", error.message) |
@@ -268,8 +282,11 @@ def test_send_request_raises_error_on_closed_stdout |
268 | 282 | stdout_write.close |
269 | 283 | end |
270 | 284 |
|
271 | | - error = assert_raises(RequestHandlerError) do |
272 | | - transport.send_request(request: request) |
| 285 | + error = nil |
| 286 | + assert_implicit_connect_deprecation_warning do |
| 287 | + error = assert_raises(RequestHandlerError) do |
| 288 | + transport.send_request(request: request) |
| 289 | + end |
273 | 290 | end |
274 | 291 |
|
275 | 292 | assert_equal("Server process closed stdout unexpectedly", error.message) |
@@ -380,7 +397,9 @@ def test_send_request_skips_initialization_on_second_call |
380 | 397 | stdout_write.flush |
381 | 398 | end |
382 | 399 |
|
383 | | - transport.send_request(request: { jsonrpc: "2.0", id: "first", method: "tools/list" }) |
| 400 | + assert_implicit_connect_deprecation_warning do |
| 401 | + transport.send_request(request: { jsonrpc: "2.0", id: "first", method: "tools/list" }) |
| 402 | + end |
384 | 403 | transport.send_request(request: { jsonrpc: "2.0", id: "second", method: "tools/list" }) |
385 | 404 |
|
386 | 405 | assert_equal( |
@@ -444,8 +463,11 @@ def test_send_request_raises_error_on_invalid_json |
444 | 463 | stdout_write.flush |
445 | 464 | end |
446 | 465 |
|
447 | | - error = assert_raises(RequestHandlerError) do |
448 | | - transport.send_request(request: request) |
| 466 | + error = nil |
| 467 | + assert_implicit_connect_deprecation_warning do |
| 468 | + error = assert_raises(RequestHandlerError) do |
| 469 | + transport.send_request(request: request) |
| 470 | + end |
449 | 471 | end |
450 | 472 |
|
451 | 473 | assert_equal("Failed to parse server response", error.message) |
@@ -486,8 +508,11 @@ def test_send_request_raises_error_when_initialization_fails |
486 | 508 | stdout_write.flush |
487 | 509 | end |
488 | 510 |
|
489 | | - error = assert_raises(RequestHandlerError) do |
490 | | - transport.send_request(request: request) |
| 511 | + error = nil |
| 512 | + assert_implicit_connect_deprecation_warning do |
| 513 | + error = assert_raises(RequestHandlerError) do |
| 514 | + transport.send_request(request: request) |
| 515 | + end |
491 | 516 | end |
492 | 517 |
|
493 | 518 | assert_equal("Server initialization failed: Invalid Request", error.message) |
@@ -563,8 +588,11 @@ def test_read_response_raises_error_on_timeout |
563 | 588 | stdin_read.gets |
564 | 589 | end |
565 | 590 |
|
566 | | - error = assert_raises(RequestHandlerError) do |
567 | | - transport.send_request(request: request) |
| 591 | + error = nil |
| 592 | + assert_implicit_connect_deprecation_warning do |
| 593 | + error = assert_raises(RequestHandlerError) do |
| 594 | + transport.send_request(request: request) |
| 595 | + end |
568 | 596 | end |
569 | 597 |
|
570 | 598 | assert_equal("Timed out waiting for server response", error.message) |
@@ -616,7 +644,9 @@ def test_send_request_raises_error_when_stdin_is_closed |
616 | 644 | end |
617 | 645 |
|
618 | 646 | # Complete handshake with a successful request |
619 | | - transport.send_request(request: { jsonrpc: "2.0", id: "setup", method: "ping" }) |
| 647 | + assert_implicit_connect_deprecation_warning do |
| 648 | + transport.send_request(request: { jsonrpc: "2.0", id: "setup", method: "ping" }) |
| 649 | + end |
620 | 650 | server_thread.join |
621 | 651 |
|
622 | 652 | # Now close stdin to simulate broken pipe |
@@ -708,8 +738,11 @@ def test_send_request_raises_error_for_missing_result |
708 | 738 | stdout_write.flush |
709 | 739 | end |
710 | 740 |
|
711 | | - error = assert_raises(RequestHandlerError) do |
712 | | - transport.send_request(request: request) |
| 741 | + error = nil |
| 742 | + assert_implicit_connect_deprecation_warning do |
| 743 | + error = assert_raises(RequestHandlerError) do |
| 744 | + transport.send_request(request: request) |
| 745 | + end |
713 | 746 | end |
714 | 747 |
|
715 | 748 | assert_equal("Server initialization failed: missing result in response", error.message) |
@@ -768,6 +801,59 @@ def test_connect_performs_initialize_handshake_explicitly |
768 | 801 | stdout_write.close |
769 | 802 | end |
770 | 803 |
|
| 804 | + def test_send_request_does_not_warn_after_explicit_connect |
| 805 | + stdin_read, stdin_write = IO.pipe |
| 806 | + stdout_read, stdout_write = IO.pipe |
| 807 | + stderr_read, _ = IO.pipe |
| 808 | + |
| 809 | + Open3.stubs(:popen3).returns([stdin_write, stdout_read, stderr_read, mock_wait_thread]) |
| 810 | + |
| 811 | + transport = Stdio.new(command: "ruby", args: ["server.rb"]) |
| 812 | + |
| 813 | + server_thread = Thread.new do |
| 814 | + init_line = stdin_read.gets |
| 815 | + init_request = JSON.parse(init_line) |
| 816 | + stdout_write.puts(JSON.generate( |
| 817 | + jsonrpc: "2.0", |
| 818 | + id: init_request["id"], |
| 819 | + result: { |
| 820 | + protocolVersion: "2025-11-25", |
| 821 | + capabilities: {}, |
| 822 | + serverInfo: { name: "test-server", version: "1.0.0" }, |
| 823 | + }, |
| 824 | + )) |
| 825 | + stdout_write.flush |
| 826 | + |
| 827 | + stdin_read.gets |
| 828 | + |
| 829 | + ping_line = stdin_read.gets |
| 830 | + ping_request = JSON.parse(ping_line) |
| 831 | + stdout_write.puts(JSON.generate( |
| 832 | + jsonrpc: "2.0", |
| 833 | + id: ping_request["id"], |
| 834 | + result: {}, |
| 835 | + )) |
| 836 | + stdout_write.flush |
| 837 | + end |
| 838 | + |
| 839 | + assert_silent do |
| 840 | + transport.connect |
| 841 | + end |
| 842 | + |
| 843 | + response = nil |
| 844 | + assert_silent do |
| 845 | + response = transport.send_request(request: { jsonrpc: "2.0", id: "ping-id", method: "ping" }) |
| 846 | + end |
| 847 | + |
| 848 | + assert_equal("ping-id", response["id"]) |
| 849 | + ensure |
| 850 | + server_thread.join |
| 851 | + stdin_read.close |
| 852 | + stdin_write.close |
| 853 | + stdout_read.close |
| 854 | + stdout_write.close |
| 855 | + end |
| 856 | + |
771 | 857 | def test_connect_caches_server_info |
772 | 858 | transport, server_thread, pipes = stub_successful_connect |
773 | 859 |
|
@@ -1141,6 +1227,14 @@ def test_server_info_is_cleared_after_close |
1141 | 1227 |
|
1142 | 1228 | private |
1143 | 1229 |
|
| 1230 | + def assert_implicit_connect_deprecation_warning(&block) |
| 1231 | + original_verbose = $VERBOSE |
| 1232 | + $VERBOSE = false |
| 1233 | + assert_output(nil, IMPLICIT_CONNECT_DEPRECATION_WARNING, &block) |
| 1234 | + ensure |
| 1235 | + $VERBOSE = original_verbose |
| 1236 | + end |
| 1237 | + |
1144 | 1238 | def stub_successful_connect |
1145 | 1239 | stdin_read, stdin_write = IO.pipe |
1146 | 1240 | stdout_read, stdout_write = IO.pipe |
|
0 commit comments