Skip to content

Commit 65d2b29

Browse files
anara123Anar Azadaliyevclaude
authored
fix(server): remove initialized notification gate to support Streamable HTTP (#788)
* fix(server): remove initialized notification gate to support Streamable HTTP The server's init handshake loop fatally rejected any request arriving before the `notifications/initialized` message. This breaks Streamable HTTP clients where each JSON-RPC message is a separate POST with no ordering guarantee — `tools/list` can easily arrive before `initialized`. Remove the ~40-line wait loop and enter `serve_inner` immediately after sending `InitializeResult`. The `initialized` notification is now handled as a regular notification by the main service loop, matching the TypeScript SDK behavior (validated in typescript-sdk#578). Also remove the now-unreachable `ExpectedInitializedNotification` error variant from `ServerInitializeError`. Closes #783 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(server): keep ExpectedInitializedNotification as deprecated Retain the variant for semver compatibility — removing it would be a breaking change caught by cargo-semver-checks. Mark it deprecated with a note that it is never constructed and will be removed in a future major release. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Anar Azadaliyev <anar.azadaliye@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a7b5700 commit 65d2b29

File tree

3 files changed

+78
-54
lines changed

3 files changed

+78
-54
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ __pycache__/
2727
# and can be added to the global gitignore or merged into this file. For a more nuclear
2828
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
2929
#.idea/
30+
node_modules/
31+
.DS_Store

crates/rmcp/src/service/server.rs

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ pub enum ServerInitializeError {
5353
#[error("expect initialized request, but received: {0:?}")]
5454
ExpectedInitializeRequest(Option<ClientJsonRpcMessage>),
5555

56+
#[deprecated(
57+
since = "1.4.0",
58+
note = "The server no longer gates on the initialized notification. This variant is never constructed and will be removed in a future major release."
59+
)]
5660
#[error("expect initialized notification, but received: {0:?}")]
5761
ExpectedInitializedNotification(Option<ClientJsonRpcMessage>),
5862

@@ -243,49 +247,12 @@ where
243247
ServerInitializeError::transport::<T>(error, "sending initialize response")
244248
})?;
245249

246-
// Wait for initialized notification. The MCP spec permits logging/setLevel and ping
247-
// before initialized; VS Code sends setLevel immediately after the initialize response.
248-
let notification = loop {
249-
let msg = expect_next_message(&mut transport, "initialize notification").await?;
250-
match msg {
251-
ClientJsonRpcMessage::Notification(n)
252-
if matches!(
253-
n.notification,
254-
ClientNotification::InitializedNotification(_)
255-
) =>
256-
{
257-
break n.notification;
258-
}
259-
ClientJsonRpcMessage::Request(req)
260-
if matches!(
261-
req.request,
262-
ClientRequest::SetLevelRequest(_) | ClientRequest::PingRequest(_)
263-
) =>
264-
{
265-
transport
266-
.send(ServerJsonRpcMessage::response(
267-
ServerResult::EmptyResult(EmptyResult {}),
268-
req.id,
269-
))
270-
.await
271-
.map_err(|error| {
272-
ServerInitializeError::transport::<T>(error, "sending pre-init response")
273-
})?;
274-
}
275-
other => {
276-
return Err(ServerInitializeError::ExpectedInitializedNotification(
277-
Some(other),
278-
));
279-
}
280-
}
281-
};
282-
let context = NotificationContext {
283-
meta: notification.get_meta().clone(),
284-
extensions: notification.extensions().clone(),
285-
peer: peer.clone(),
286-
};
287-
let _ = service.handle_notification(notification, context).await;
288-
// Continue processing service
250+
// Enter the main service loop immediately after sending InitializeResult.
251+
// The initialized notification will be handled as a regular notification by serve_inner.
252+
// This matches the TypeScript SDK behavior: no init gate, no waiting for initialized.
253+
// Streamable HTTP has no ordering guarantee between POSTs, and the MCP spec uses
254+
// SHOULD NOT (not MUST NOT) for pre-initialized messages, so any request arriving
255+
// before initialized is processed normally.
289256
Ok(serve_inner(service, transport, peer, peer_rx, ct))
290257
}
291258

crates/rmcp/tests/test_server_initialization.rs

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use common::handlers::TestServer;
66
use rmcp::{
77
ServiceExt,
88
model::{ClientJsonRpcMessage, ServerJsonRpcMessage, ServerResult},
9-
service::ServerInitializeError,
109
transport::{IntoTransport, Transport},
1110
};
1211

@@ -54,7 +53,7 @@ async fn do_initialize(client: &mut impl Transport<rmcp::RoleClient>) {
5453
let _response = client.receive().await.unwrap();
5554
}
5655

57-
// Server responds with EmptyResult to setLevel received before initialized.
56+
// Server handles setLevel sent before initialized notification (processed by serve_inner).
5857
#[tokio::test]
5958
async fn server_init_set_level_response_is_empty_result() {
6059
let (server_transport, client_transport) = tokio::io::duplex(4096);
@@ -64,7 +63,14 @@ async fn server_init_set_level_response_is_empty_result() {
6463
do_initialize(&mut client).await;
6564
client.send(set_level_request(2)).await.unwrap();
6665

67-
let response = client.receive().await.unwrap();
66+
// The handler may send logging notifications before the response;
67+
// skip notifications to find the EmptyResult response.
68+
let response = loop {
69+
let msg = client.receive().await.unwrap();
70+
if matches!(msg, ServerJsonRpcMessage::Response(_)) {
71+
break msg;
72+
}
73+
};
6874
assert!(
6975
matches!(
7076
response,
@@ -85,7 +91,13 @@ async fn server_init_succeeds_after_set_level_before_initialized() {
8591

8692
do_initialize(&mut client).await;
8793
client.send(set_level_request(2)).await.unwrap();
88-
let _response = client.receive().await.unwrap();
94+
// Skip notifications until we get the response
95+
loop {
96+
let msg = client.receive().await.unwrap();
97+
if matches!(msg, ServerJsonRpcMessage::Response(_)) {
98+
break;
99+
}
100+
}
89101
client.send(initialized_notification()).await.unwrap();
90102

91103
let result = server_handle.await.unwrap();
@@ -179,23 +191,66 @@ async fn server_init_succeeds_after_ping_before_initialized() {
179191
result.unwrap().cancel().await.unwrap();
180192
}
181193

182-
// Server returns ExpectedInitializedNotification for any other message before initialized.
194+
// Server buffers tools/list sent before initialized and processes it after initialization.
183195
#[tokio::test]
184-
async fn server_init_rejects_unexpected_message_before_initialized() {
196+
async fn server_init_buffers_request_before_initialized() {
185197
let (server_transport, client_transport) = tokio::io::duplex(4096);
186198
let server_handle =
187199
tokio::spawn(async move { TestServer::new().serve(server_transport).await });
188200
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
189201

190202
do_initialize(&mut client).await;
203+
// Send tools/list before initialized notification
191204
client.send(list_tools_request(2)).await.unwrap();
205+
// Now send initialized notification
206+
client.send(initialized_notification()).await.unwrap();
207+
208+
// The buffered tools/list should be processed — expect a response
209+
let response = client.receive().await.unwrap();
210+
assert!(
211+
matches!(response, ServerJsonRpcMessage::Response(_)),
212+
"expected response for buffered tools/list, got: {response:?}"
213+
);
192214

193215
let result = server_handle.await.unwrap();
194216
assert!(
195-
matches!(
196-
result,
197-
Err(ServerInitializeError::ExpectedInitializedNotification(_))
198-
),
199-
"expected ExpectedInitializedNotification error"
217+
result.is_ok(),
218+
"server should initialize successfully when buffering pre-init messages"
200219
);
220+
result.unwrap().cancel().await.unwrap();
221+
}
222+
223+
// Server buffers multiple requests before initialized and processes them in order.
224+
#[tokio::test]
225+
async fn server_init_buffers_multiple_requests_before_initialized() {
226+
let (server_transport, client_transport) = tokio::io::duplex(4096);
227+
let server_handle =
228+
tokio::spawn(async move { TestServer::new().serve(server_transport).await });
229+
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
230+
231+
do_initialize(&mut client).await;
232+
// Send two requests before initialized
233+
client.send(list_tools_request(2)).await.unwrap();
234+
client.send(ping_request(3)).await.unwrap();
235+
// Now send initialized notification
236+
client.send(initialized_notification()).await.unwrap();
237+
238+
// Both buffered messages should get responses
239+
let response1 = client.receive().await.unwrap();
240+
let response2 = client.receive().await.unwrap();
241+
assert!(
242+
matches!(response1, ServerJsonRpcMessage::Response(_)),
243+
"expected response for first buffered message, got: {response1:?}"
244+
);
245+
assert!(
246+
matches!(response2, ServerJsonRpcMessage::Response(_)),
247+
"expected response for second buffered message, got: {response2:?}"
248+
);
249+
250+
let result = server_handle.await.unwrap();
251+
assert!(
252+
result.is_ok(),
253+
"server should initialize successfully with multiple buffered messages"
254+
);
255+
result.unwrap().cancel().await.unwrap();
201256
}

0 commit comments

Comments
 (0)