Skip to content

Commit 702cf76

Browse files
authored
fix(jetsocat): missing newline after MCP proxy JSON-RPC responses (#1507)
1 parent 91f22f9 commit 702cf76

File tree

3 files changed

+81
-37
lines changed

3 files changed

+81
-37
lines changed

crates/mcp-proxy/src/lib.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ impl JsonRpcResponse {
5151

5252
if let Some(id) = self.id {
5353
obj.insert("id".to_owned(), tinyjson::JsonValue::Number(id as f64));
54-
} else {
55-
obj.insert("id".to_owned(), tinyjson::JsonValue::Null);
5654
}
5755

5856
if let Some(result) = &self.result {
@@ -64,7 +62,7 @@ impl JsonRpcResponse {
6462
}
6563

6664
let json_obj = tinyjson::JsonValue::Object(obj);
67-
Ok(json_obj.stringify().unwrap())
65+
json_obj.stringify().context("failed to stringify response")
6866
}
6967
}
7068

@@ -184,6 +182,11 @@ impl McpProxy {
184182

185183
Ok(None)
186184
}
185+
"logging/setLevel" => {
186+
debug!("Received logging/setLevel with parameters {:?}", request.params);
187+
188+
Ok(None)
189+
}
187190
"tools/list" => {
188191
match &self.transport {
189192
InnerTransport::Http { url, .. } => {

crates/mcp-proxy/tests/no_io.rs

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#![allow(clippy::unwrap_used)]
33

44
use mcp_proxy::internal::{decode_content_texts, extract_sse_json_line, unwrap_json_rpc_inner_result};
5-
use mcp_proxy::{Config, JsonRpcRequest, McpProxy};
5+
use mcp_proxy::{Config, JsonRpcRequest, JsonRpcResponse, McpProxy};
66

77
use std::collections::HashMap;
88

@@ -50,18 +50,18 @@ fn sse_no_data_is_none() {
5050
fn decode_escaped_texts_works() {
5151
let mut text_obj = HashMap::new();
5252
text_obj.insert(
53-
"text".to_string(),
54-
tinyjson::JsonValue::String("hello\\u0027world\\ncode:\\u003Ctag\\u003E".to_string()),
53+
"text".to_owned(),
54+
tinyjson::JsonValue::String("hello\\u0027world\\ncode:\\u003Ctag\\u003E".to_owned()),
5555
);
5656

5757
let mut content_obj = HashMap::new();
5858
content_obj.insert(
59-
"content".to_string(),
59+
"content".to_owned(),
6060
tinyjson::JsonValue::Array(vec![tinyjson::JsonValue::Object(text_obj)]),
6161
);
6262

6363
let mut result_obj = HashMap::new();
64-
result_obj.insert("result".to_string(), tinyjson::JsonValue::Object(content_obj));
64+
result_obj.insert("result".to_owned(), tinyjson::JsonValue::Object(content_obj));
6565

6666
let mut v = tinyjson::JsonValue::Object(result_obj);
6767
decode_content_texts(&mut v);
@@ -75,10 +75,10 @@ fn decode_escaped_texts_works() {
7575
#[test]
7676
fn unwrap_json_rpc_inner_result_prefers_result() {
7777
let mut tools_obj = HashMap::new();
78-
tools_obj.insert("tools".to_string(), tinyjson::JsonValue::Array(vec![]));
78+
tools_obj.insert("tools".to_owned(), tinyjson::JsonValue::Array(vec![]));
7979

8080
let mut v_obj = HashMap::new();
81-
v_obj.insert("result".to_string(), tinyjson::JsonValue::Object(tools_obj.clone()));
81+
v_obj.insert("result".to_owned(), tinyjson::JsonValue::Object(tools_obj.clone()));
8282

8383
let v = tinyjson::JsonValue::Object(v_obj);
8484
let got = unwrap_json_rpc_inner_result(v);
@@ -90,7 +90,7 @@ fn unwrap_json_rpc_inner_result_prefers_result() {
9090
#[test]
9191
fn unwrap_json_rpc_inner_result_passthrough() {
9292
let mut v_obj = HashMap::new();
93-
v_obj.insert("tools".to_string(), tinyjson::JsonValue::Array(vec![]));
93+
v_obj.insert("tools".to_owned(), tinyjson::JsonValue::Array(vec![]));
9494

9595
let v = tinyjson::JsonValue::Object(v_obj);
9696
let got = unwrap_json_rpc_inner_result(v.clone());
@@ -137,3 +137,40 @@ async fn unknown_method_is_32601() {
137137
.unwrap();
138138
assert_eq!(get_number_path(resp.error.as_ref().unwrap(), &["code"]), -32601.0);
139139
}
140+
141+
#[test]
142+
fn response_omits_null_id() {
143+
let response = JsonRpcResponse {
144+
jsonrpc: "2.0".to_owned(),
145+
id: None,
146+
result: Some(tinyjson::JsonValue::String("test".to_owned())),
147+
error: None,
148+
};
149+
150+
let json_str = response.to_string().unwrap();
151+
let parsed: tinyjson::JsonValue = json_str.parse().unwrap();
152+
let obj = parsed.get::<HashMap<String, tinyjson::JsonValue>>().unwrap();
153+
154+
// The id field should not be present when None
155+
assert!(!obj.contains_key("id"));
156+
assert!(obj.contains_key("jsonrpc"));
157+
assert!(obj.contains_key("result"));
158+
}
159+
160+
#[test]
161+
fn response_includes_id_when_present() {
162+
let response = JsonRpcResponse {
163+
jsonrpc: "2.0".to_owned(),
164+
id: Some(42),
165+
result: Some(tinyjson::JsonValue::String("test".to_owned())),
166+
error: None,
167+
};
168+
169+
let json_str = response.to_string().unwrap();
170+
let parsed: tinyjson::JsonValue = json_str.parse().unwrap();
171+
let obj = parsed.get::<HashMap<String, tinyjson::JsonValue>>().unwrap();
172+
173+
// The id field should be present when Some
174+
assert!(obj.contains_key("id"));
175+
assert_eq!(obj.get("id").unwrap().get::<f64>().unwrap(), &42.0);
176+
}

jetsocat/src/mcp.rs

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,37 @@ pub(crate) async fn run_mcp_proxy(pipe: Pipe, mut mcp_client: mcp_proxy::McpProx
1313
loop {
1414
line.clear();
1515

16-
match reader.read_line(&mut line).await? {
17-
0 => break, // EOF
18-
_ => {
19-
let line = line.trim();
20-
21-
if line.is_empty() {
22-
continue;
23-
}
24-
25-
trace!(request = %line, "Received request");
26-
27-
match mcp_client.handle_jsonrpc_request_str(line).await {
28-
Ok(Some(resp)) => {
29-
let response = resp.to_string()?;
30-
writer
31-
.write_all(response.as_bytes())
32-
.await
33-
.context("failed to write response")?;
34-
}
35-
Ok(None) => {} // Notification; no response.
36-
Err(e) => {
37-
error!(error = format!("{e:#}"), "failed to handle request");
38-
}
39-
}
16+
let n_read = reader.read_line(&mut line).await.context("read_line")?;
17+
18+
if n_read == 0 {
19+
debug!("Pipe EOFed");
20+
return Ok(());
21+
}
22+
23+
let line = line.trim();
24+
25+
if line.is_empty() {
26+
continue;
27+
}
28+
29+
trace!(request = %line, "Received request");
30+
31+
match mcp_client.handle_jsonrpc_request_str(line).await {
32+
Ok(Some(resp)) => {
33+
let mut response = resp.to_string()?;
34+
trace!(%response, "Sending response");
35+
response.push('\n'); // Push a newline to delemitate the message.
36+
37+
writer
38+
.write_all(response.as_bytes())
39+
.await
40+
.context("failed to write response")?;
41+
writer.flush().await.context("failed to flush writer")?;
42+
}
43+
Ok(None) => {} // Notification; no response.
44+
Err(e) => {
45+
error!(error = format!("{e:#}"), "failed to handle request");
4046
}
4147
}
4248
}
43-
44-
Ok(())
4549
}

0 commit comments

Comments
 (0)