Skip to content

Commit b6462fe

Browse files
committed
feat: warn on implicit stdio initialization
1 parent dd7daca commit b6462fe

2 files changed

Lines changed: 116 additions & 19 deletions

File tree

lib/mcp/client/stdio.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ def connected?
134134

135135
def send_request(request:)
136136
start unless @started
137-
connect unless @initialized
137+
unless @initialized
138+
warn("Calling `MCP::Client::Stdio#send_request` without calling `MCP::Client#connect` is deprecated. Use `MCP::Client#connect` before sending requests instead.", uplevel: 1)
139+
connect
140+
end
138141

139142
write_message(request)
140143
read_response(request)

test/mcp/client/stdio_test.rb

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
module MCP
1010
class Client
1111
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+
1215
def test_send_request_starts_process_and_returns_response
1316
stdin_read, stdin_write = IO.pipe
1417
stdout_read, stdout_write = IO.pipe
@@ -56,7 +59,10 @@ def test_send_request_starts_process_and_returns_response
5659
stdout_write.flush
5760
end
5861

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
6066

6167
assert_equal("test-id", response["id"])
6268
assert_equal(1, response.dig("result", "tools").size)
@@ -123,7 +129,9 @@ def test_send_request_initializes_session_on_first_call
123129
stdout_write.flush
124130
end
125131

126-
transport.send_request(request: request)
132+
assert_implicit_connect_deprecation_warning do
133+
transport.send_request(request: request)
134+
end
127135

128136
assert_equal(["initialize", "notifications/initialized", "tools/list"], received_methods)
129137
ensure
@@ -184,10 +192,13 @@ def test_send_request_skips_notifications
184192
stdout_write.flush
185193
end
186194

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
188199

189200
assert_equal("test-id", response["id"])
190-
assert_equal([], response.dig("result", "tools"))
201+
assert_empty(response.dig("result", "tools"))
191202
ensure
192203
server_thread.join
193204
stdin_read.close
@@ -217,8 +228,11 @@ def test_send_request_raises_error_when_process_exits
217228
method: "tools/list",
218229
}
219230

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
222236
end
223237

224238
assert_equal("Server process has exited", error.message)
@@ -268,8 +282,11 @@ def test_send_request_raises_error_on_closed_stdout
268282
stdout_write.close
269283
end
270284

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
273290
end
274291

275292
assert_equal("Server process closed stdout unexpectedly", error.message)
@@ -380,7 +397,9 @@ def test_send_request_skips_initialization_on_second_call
380397
stdout_write.flush
381398
end
382399

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
384403
transport.send_request(request: { jsonrpc: "2.0", id: "second", method: "tools/list" })
385404

386405
assert_equal(
@@ -444,8 +463,11 @@ def test_send_request_raises_error_on_invalid_json
444463
stdout_write.flush
445464
end
446465

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
449471
end
450472

451473
assert_equal("Failed to parse server response", error.message)
@@ -486,8 +508,11 @@ def test_send_request_raises_error_when_initialization_fails
486508
stdout_write.flush
487509
end
488510

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
491516
end
492517

493518
assert_equal("Server initialization failed: Invalid Request", error.message)
@@ -563,8 +588,11 @@ def test_read_response_raises_error_on_timeout
563588
stdin_read.gets
564589
end
565590

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
568596
end
569597

570598
assert_equal("Timed out waiting for server response", error.message)
@@ -616,7 +644,9 @@ def test_send_request_raises_error_when_stdin_is_closed
616644
end
617645

618646
# 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
620650
server_thread.join
621651

622652
# Now close stdin to simulate broken pipe
@@ -708,8 +738,11 @@ def test_send_request_raises_error_for_missing_result
708738
stdout_write.flush
709739
end
710740

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
713746
end
714747

715748
assert_equal("Server initialization failed: missing result in response", error.message)
@@ -768,6 +801,59 @@ def test_connect_performs_initialize_handshake_explicitly
768801
stdout_write.close
769802
end
770803

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+
771857
def test_connect_caches_server_info
772858
transport, server_thread, pipes = stub_successful_connect
773859

@@ -1141,6 +1227,14 @@ def test_server_info_is_cleared_after_close
11411227

11421228
private
11431229

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+
11441238
def stub_successful_connect
11451239
stdin_read, stdin_write = IO.pipe
11461240
stdout_read, stdout_write = IO.pipe

0 commit comments

Comments
 (0)