From b85cb37898291598af78f7ffc07c9a044117582e Mon Sep 17 00:00:00 2001 From: jiah0231 <2243725197@cug.edu.cn> Date: Thu, 27 Aug 2026 10:30:42 +0800 Subject: [PATCH 1/3] fix: normalize integer-valued shell timeouts --- server/src/cursor/tools/dispatch/mod.rs | 114 ++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/server/src/cursor/tools/dispatch/mod.rs b/server/src/cursor/tools/dispatch/mod.rs index eb4ea88..217c8de 100644 --- a/server/src/cursor/tools/dispatch/mod.rs +++ b/server/src/cursor/tools/dispatch/mod.rs @@ -39,6 +39,9 @@ pub(super) async fn start( context: &ExecContext, store: Option<&Store>, ) -> Result { + let normalized_call = normalize_block_until_ms(call)?; + let call = normalized_call.as_ref().unwrap_or(call); + if let Some(definition) = dynamic_mcp.get(&call.name) { return exec::start_dynamic(runtime, call, definition, context).await; } @@ -64,6 +67,55 @@ pub(super) async fn start( } } +fn normalize_block_until_ms(call: &ToolCall) -> Result> { + if !matches!(normalized(&call.name).as_str(), "shell" | "awaitshell") { + return Ok(None); + } + let Some(value) = call.arguments.get("block_until_ms") else { + return Ok(None); + }; + + let integer = if let Some(value) = value.as_i64() { + value + } else { + let value = value.as_f64().ok_or_else(|| { + Error::Protocol(format!("{} block_until_ms must be an integer", call.name)) + })?; + if !value.is_finite() || value.fract() != 0.0 { + return Err(Error::Protocol(format!( + "{} block_until_ms must be an integer", + call.name + ))); + } + if value < i64::MIN as f64 || value > i64::MAX as f64 { + return Err(Error::Protocol(format!( + "{} block_until_ms is out of range", + call.name + ))); + } + value as i64 + }; + + if integer < 0 { + return Err(Error::Protocol(format!( + "{} block_until_ms is out of range", + call.name + ))); + } + + if value.as_i64().is_some() { + return Ok(None); + } + + let mut normalized_call = call.clone(); + normalized_call + .arguments + .as_object_mut() + .ok_or_else(|| Error::Protocol(format!("{} arguments must be a JSON object", call.name)))? + .insert("block_until_ms".into(), serde_json::Value::from(integer)); + Ok(Some(normalized_call)) +} + fn is_mcp_auth(call: &ToolCall) -> bool { normalized(&call.name) == "callmcptool" && call @@ -89,3 +141,65 @@ pub(super) fn normalized(name: &str) -> String { .flat_map(char::to_lowercase) .collect() } + +#[cfg(test)] +mod tests { + use super::*; + + fn tool(name: &str, arguments: serde_json::Value) -> ToolCall { + ToolCall { + index: 0, + call_id: "call-1".into(), + model_call_id: "model-call-1".into(), + name: name.into(), + arguments_text: arguments.to_string(), + arguments, + } + } + + #[test] + fn shell_accepts_integer_valued_float_timeout() { + let call = tool("Shell", serde_json::json!({ + "command": "echo ok", + "block_until_ms": 45_000.0 + })); + + let call = normalize_block_until_ms(&call).unwrap().unwrap(); + + assert_eq!(call.arguments["block_until_ms"].as_i64(), Some(45_000)); + } + + #[test] + fn await_shell_accepts_integer_valued_scientific_timeout() { + let arguments = serde_json::from_str(r#"{"shell_id":"1","block_until_ms":3e4}"#).unwrap(); + let call = tool("AwaitShell", arguments); + + let call = normalize_block_until_ms(&call).unwrap().unwrap(); + + assert_eq!(call.arguments["block_until_ms"].as_u64(), Some(30_000)); + } + + #[test] + fn shell_rejects_fractional_timeout() { + let call = tool("Shell", serde_json::json!({ + "command": "echo ok", + "block_until_ms": 30_000.5 + })); + + let error = normalize_block_until_ms(&call).unwrap_err(); + + assert_eq!(error.to_string(), "protocol error: Shell block_until_ms must be an integer"); + } + + #[test] + fn await_shell_rejects_negative_timeout_instead_of_defaulting() { + let call = tool("AwaitShell", serde_json::json!({ + "shell_id": "1", + "block_until_ms": -1 + })); + + let error = normalize_block_until_ms(&call).unwrap_err(); + + assert_eq!(error.to_string(), "protocol error: AwaitShell block_until_ms is out of range"); + } +} From 7914f24d8312486caf76ac67e73ff08c22c0a6f4 Mon Sep 17 00:00:00 2001 From: jiah0231 <2243725197@cug.edu.cn> Date: Thu, 27 Aug 2026 10:31:18 +0800 Subject: [PATCH 2/3] fix: preserve dynamic MCP argument schemas --- server/src/cursor/tools/dispatch/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/cursor/tools/dispatch/mod.rs b/server/src/cursor/tools/dispatch/mod.rs index 217c8de..53ce5c1 100644 --- a/server/src/cursor/tools/dispatch/mod.rs +++ b/server/src/cursor/tools/dispatch/mod.rs @@ -39,9 +39,6 @@ pub(super) async fn start( context: &ExecContext, store: Option<&Store>, ) -> Result { - let normalized_call = normalize_block_until_ms(call)?; - let call = normalized_call.as_ref().unwrap_or(call); - if let Some(definition) = dynamic_mcp.get(&call.name) { return exec::start_dynamic(runtime, call, definition, context).await; } @@ -54,6 +51,9 @@ pub(super) async fn start( return local::subagents_disabled(call); } + let normalized_call = normalize_block_until_ms(call)?; + let call = normalized_call.as_ref().unwrap_or(call); + match normalized(&call.name).as_str() { "shell" | "read" | "delete" | "grep" | "glob" | "readlints" | "task" | "callmcptool" | "fetchmcpresource" | "getmcptools" => exec::start(runtime, call, context).await, From 02a190489a33bc87874ee4694fd7d68081f4101c Mon Sep 17 00:00:00 2001 From: Jilin Chen <162280666+Weizhena@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:23:58 +0800 Subject: [PATCH 3/3] fix: accept reasoning field in OpenAI Chat streams --- server/src/provider/openai_chat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/provider/openai_chat.rs b/server/src/provider/openai_chat.rs index bea7cb2..bdbd16b 100644 --- a/server/src/provider/openai_chat.rs +++ b/server/src/provider/openai_chat.rs @@ -132,7 +132,7 @@ impl Provider for OpenAiChatProvider { } let Some(choice) = value.get("choices").and_then(Value::as_array).and_then(|values| values.first()) else { continue; }; let delta = choice.get("delta").unwrap_or(&Value::Null); - if let Some(reasoning_delta) = delta.get("reasoning_content").and_then(Value::as_str).filter(|text| !text.is_empty()) { + if let Some(reasoning_delta) = delta.get("reasoning_content").or_else(|| delta.get("reasoning")).and_then(Value::as_str).filter(|text| !text.is_empty()) { if !thinking_open { thinking_open = true; yield ModelEvent::ThinkingStart; } reasoning.push_str(reasoning_delta); yield ModelEvent::ThinkingDelta(reasoning_delta.into());