Skip to content

Commit 8e5ebb4

Browse files
authored
fix: accept logging/setLevel and ping before initialized notification (#730)
* fix: accept logging/setLevel and ping before initialized notification * test: add server initialization tests for pre-init requests
1 parent 9b507f5 commit 8e5ebb4

2 files changed

Lines changed: 196 additions & 23 deletions

File tree

‎crates/rmcp/src/service/server.rs‎

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::{
1616
model::{
1717
CancelledNotification, CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage,
1818
ClientNotification, ClientRequest, ClientResult, CreateMessageRequest,
19-
CreateMessageRequestParams, CreateMessageResult, ErrorData, ListRootsRequest,
19+
CreateMessageRequestParams, CreateMessageResult, EmptyResult, ErrorData, ListRootsRequest,
2020
ListRootsResult, LoggingMessageNotification, LoggingMessageNotificationParam,
2121
ProgressNotification, ProgressNotificationParam, PromptListChangedNotification,
2222
ProtocolVersion, ResourceListChangedNotification, ResourceUpdatedNotification,
@@ -147,22 +147,6 @@ where
147147
)))
148148
}
149149

150-
/// Helper function to expect a notification from the stream
151-
async fn expect_notification<T>(
152-
transport: &mut T,
153-
context: &str,
154-
) -> Result<ClientNotification, ServerInitializeError>
155-
where
156-
T: Transport<RoleServer>,
157-
{
158-
let msg = expect_next_message(transport, context).await?;
159-
let msg_clone = msg.clone();
160-
msg.into_notification()
161-
.ok_or(ServerInitializeError::ExpectedInitializedNotification(
162-
Some(msg_clone),
163-
))
164-
}
165-
166150
pub async fn serve_server_with_ct<S, T, E, A>(
167151
service: S,
168152
transport: T,
@@ -246,12 +230,41 @@ where
246230
ServerInitializeError::transport::<T>(error, "sending initialize response")
247231
})?;
248232

249-
// Wait for initialize notification
250-
let notification = expect_notification(&mut transport, "initialize notification").await?;
251-
let ClientNotification::InitializedNotification(_) = notification else {
252-
return Err(ServerInitializeError::ExpectedInitializedNotification(
253-
Some(ClientJsonRpcMessage::notification(notification)),
254-
));
233+
// Wait for initialized notification. The MCP spec permits logging/setLevel and ping
234+
// before initialized; VS Code sends setLevel immediately after the initialize response.
235+
let notification = loop {
236+
let msg = expect_next_message(&mut transport, "initialize notification").await?;
237+
match msg {
238+
ClientJsonRpcMessage::Notification(n)
239+
if matches!(
240+
n.notification,
241+
ClientNotification::InitializedNotification(_)
242+
) =>
243+
{
244+
break n.notification;
245+
}
246+
ClientJsonRpcMessage::Request(req)
247+
if matches!(
248+
req.request,
249+
ClientRequest::SetLevelRequest(_) | ClientRequest::PingRequest(_)
250+
) =>
251+
{
252+
transport
253+
.send(ServerJsonRpcMessage::response(
254+
ServerResult::EmptyResult(EmptyResult {}),
255+
req.id,
256+
))
257+
.await
258+
.map_err(|error| {
259+
ServerInitializeError::transport::<T>(error, "sending pre-init response")
260+
})?;
261+
}
262+
other => {
263+
return Err(ServerInitializeError::ExpectedInitializedNotification(
264+
Some(other),
265+
));
266+
}
267+
}
255268
};
256269
let context = NotificationContext {
257270
meta: notification.get_meta().clone(),
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// cargo test --features "client" --package rmcp -- server_init
2+
#![cfg(feature = "client")]
3+
mod common;
4+
5+
use common::handlers::TestServer;
6+
use rmcp::{
7+
ServiceExt,
8+
model::{ClientJsonRpcMessage, ServerJsonRpcMessage, ServerResult},
9+
service::ServerInitializeError,
10+
transport::{IntoTransport, Transport},
11+
};
12+
13+
fn msg(raw: &str) -> ClientJsonRpcMessage {
14+
serde_json::from_str(raw).expect("invalid test message JSON")
15+
}
16+
17+
fn init_request() -> ClientJsonRpcMessage {
18+
msg(r#"{
19+
"jsonrpc": "2.0",
20+
"id": 1,
21+
"method": "initialize",
22+
"params": {
23+
"protocolVersion": "2025-11-25",
24+
"capabilities": {},
25+
"clientInfo": { "name": "test-client", "version": "0.0.1" }
26+
}
27+
}"#)
28+
}
29+
30+
fn initialized_notification() -> ClientJsonRpcMessage {
31+
msg(r#"{ "jsonrpc": "2.0", "method": "notifications/initialized" }"#)
32+
}
33+
34+
fn set_level_request(id: u64) -> ClientJsonRpcMessage {
35+
msg(&format!(
36+
r#"{{ "jsonrpc": "2.0", "id": {id}, "method": "logging/setLevel", "params": {{ "level": "info" }} }}"#
37+
))
38+
}
39+
40+
fn ping_request(id: u64) -> ClientJsonRpcMessage {
41+
msg(&format!(
42+
r#"{{ "jsonrpc": "2.0", "id": {id}, "method": "ping" }}"#
43+
))
44+
}
45+
46+
fn list_tools_request(id: u64) -> ClientJsonRpcMessage {
47+
msg(&format!(
48+
r#"{{ "jsonrpc": "2.0", "id": {id}, "method": "tools/list" }}"#
49+
))
50+
}
51+
52+
async fn do_initialize(client: &mut impl Transport<rmcp::RoleClient>) {
53+
client.send(init_request()).await.unwrap();
54+
let _response = client.receive().await.unwrap();
55+
}
56+
57+
// Server responds with EmptyResult to setLevel received before initialized.
58+
#[tokio::test]
59+
async fn server_init_set_level_response_is_empty_result() {
60+
let (server_transport, client_transport) = tokio::io::duplex(4096);
61+
let _server = tokio::spawn(async move { TestServer::new().serve(server_transport).await });
62+
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
63+
64+
do_initialize(&mut client).await;
65+
client.send(set_level_request(2)).await.unwrap();
66+
67+
let response = client.receive().await.unwrap();
68+
assert!(
69+
matches!(
70+
response,
71+
ServerJsonRpcMessage::Response(ref r)
72+
if matches!(r.result, ServerResult::EmptyResult(_))
73+
),
74+
"expected EmptyResult for setLevel, got: {response:?}"
75+
);
76+
}
77+
78+
// Server initializes successfully when setLevel is sent before the initialized notification.
79+
#[tokio::test]
80+
async fn server_init_succeeds_after_set_level_before_initialized() {
81+
let (server_transport, client_transport) = tokio::io::duplex(4096);
82+
let server_handle =
83+
tokio::spawn(async move { TestServer::new().serve(server_transport).await });
84+
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
85+
86+
do_initialize(&mut client).await;
87+
client.send(set_level_request(2)).await.unwrap();
88+
let _response = client.receive().await.unwrap();
89+
client.send(initialized_notification()).await.unwrap();
90+
91+
let result = server_handle.await.unwrap();
92+
assert!(
93+
result.is_ok(),
94+
"server should initialize successfully after setLevel"
95+
);
96+
result.unwrap().cancel().await.unwrap();
97+
}
98+
99+
// Server responds with EmptyResult to ping received before initialized.
100+
#[tokio::test]
101+
async fn server_init_ping_response_is_empty_result() {
102+
let (server_transport, client_transport) = tokio::io::duplex(4096);
103+
let _server = tokio::spawn(async move { TestServer::new().serve(server_transport).await });
104+
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
105+
106+
do_initialize(&mut client).await;
107+
client.send(ping_request(2)).await.unwrap();
108+
109+
let response = client.receive().await.unwrap();
110+
assert!(
111+
matches!(
112+
response,
113+
ServerJsonRpcMessage::Response(ref r)
114+
if matches!(r.result, ServerResult::EmptyResult(_))
115+
),
116+
"expected EmptyResult for ping, got: {response:?}"
117+
);
118+
}
119+
120+
// Server initializes successfully when ping is sent before the initialized notification.
121+
#[tokio::test]
122+
async fn server_init_succeeds_after_ping_before_initialized() {
123+
let (server_transport, client_transport) = tokio::io::duplex(4096);
124+
let server_handle =
125+
tokio::spawn(async move { TestServer::new().serve(server_transport).await });
126+
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
127+
128+
do_initialize(&mut client).await;
129+
client.send(ping_request(2)).await.unwrap();
130+
let _response = client.receive().await.unwrap();
131+
client.send(initialized_notification()).await.unwrap();
132+
133+
let result = server_handle.await.unwrap();
134+
assert!(
135+
result.is_ok(),
136+
"server should initialize successfully after ping"
137+
);
138+
result.unwrap().cancel().await.unwrap();
139+
}
140+
141+
// Server returns ExpectedInitializedNotification for any other message before initialized.
142+
#[tokio::test]
143+
async fn server_init_rejects_unexpected_message_before_initialized() {
144+
let (server_transport, client_transport) = tokio::io::duplex(4096);
145+
let server_handle =
146+
tokio::spawn(async move { TestServer::new().serve(server_transport).await });
147+
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
148+
149+
do_initialize(&mut client).await;
150+
client.send(list_tools_request(2)).await.unwrap();
151+
152+
let result = server_handle.await.unwrap();
153+
assert!(
154+
matches!(
155+
result,
156+
Err(ServerInitializeError::ExpectedInitializedNotification(_))
157+
),
158+
"expected ExpectedInitializedNotification error"
159+
);
160+
}

0 commit comments

Comments
 (0)