use crate::{ cursor::proto::agent::v1 as pb, model::{ToolCall, ToolResult}, }; use super::{codec, result::ToolCompletion, runtime::now_ms}; // Unknown/retired tools use a generic Cursor MCP card only as a wire/UI // representation; they are never dispatched to an MCP server. const COMPAT_PROVIDER: &str = "cursor-byok-compat"; pub(crate) fn placeholder(name: &str, call_id: &str) -> pb::ToolCall { pb::ToolCall { hook_additional_contexts: Vec::new(), tool_call_id: Some(call_id.into()), started_at_ms: None, completed_at_ms: None, tool: Some(pb::tool_call::Tool::McpToolCall(pb::McpToolCall { args: Some(pb::McpArgs { name: name.into(), tool_call_id: call_id.into(), provider_identifier: COMPAT_PROVIDER.into(), tool_name: name.into(), server_identifier: COMPAT_PROVIDER.into(), ..Default::default() }), result: None, description: Some("Unavailable legacy or unsupported tool".into()), })), } } pub(crate) fn render(call: &ToolCall, completed: bool) -> pb::ToolCall { let mut output = placeholder(&call.name, &call.call_id); let timestamp = now_ms(); output.started_at_ms = Some(timestamp); output.completed_at_ms = completed.then_some(timestamp); if let Some(pb::tool_call::Tool::McpToolCall(tool)) = output.tool.as_mut() { if let Some(args) = tool.args.as_mut() { args.args = call .arguments .as_object() .map(codec::json_object_to_prost) .unwrap_or_default(); } } output } pub(crate) fn failure(call: &ToolCall) -> ToolCompletion { let error = failure_message(&call.name); let arguments = call .arguments .as_object() .map(codec::json_object_to_prost) .unwrap_or_default(); ToolCompletion::new( call, now_ms(), ToolResult { call_id: call.call_id.clone(), content: error.clone(), is_error: true, image: None, }, pb::tool_call::Tool::McpToolCall(pb::McpToolCall { args: Some(pb::McpArgs { name: call.name.clone(), args: arguments, tool_call_id: call.call_id.clone(), provider_identifier: COMPAT_PROVIDER.into(), tool_name: call.name.clone(), server_identifier: COMPAT_PROVIDER.into(), ..Default::default() }), result: Some(pb::McpToolResult { result: Some(pb::mcp_tool_result::Result::Error(pb::McpToolError { error, read_tool_def_reminder: String::new(), })), }), description: Some("Unavailable legacy or unsupported tool".into()), }), ) } fn failure_message(name: &str) -> String { if normalized(name) == "awaitshell" { return "Tool \"AwaitShell\" is no longer available in this Cursor BYOK version. The model emitted a tool name that is not part of the current advertised tool set. Treat the tool call as failed and continue using only tools advertised in the current prompt; for background shell work, use the current Shell/background completion flow.".into(); } format!( "Tool \"{name}\" is not available in this Cursor BYOK version. The model emitted a tool name that is not part of the current advertised tool set. Treat the tool call as failed and continue using a tool advertised in the current prompt." ) } fn normalized(name: &str) -> String { name.chars() .filter(|character| character.is_ascii_alphanumeric()) .flat_map(char::to_lowercase) .collect() } #[cfg(test)] mod tests { use super::*; fn tool(name: &str) -> ToolCall { let arguments = serde_json::json!({"shell_id": "runtime-shell", "block_until_ms": 30000}); 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 retired_await_shell_is_a_model_visible_failure() { let completion = failure(&tool("AwaitShell")); assert!(completion.result().is_error); assert!(completion .result() .content .contains("current advertised tool set")); assert!(completion .result() .content .contains("current Shell/background completion flow")); let Some(pb::tool_call::Tool::McpToolCall(rendered)) = completion.tool_call().tool.as_ref() else { panic!("expected compatibility MCP card"); }; let args = rendered.args.as_ref().unwrap(); assert_eq!(args.provider_identifier, COMPAT_PROVIDER); assert_eq!(args.tool_name, "AwaitShell"); } #[test] fn arbitrary_unknown_tool_is_a_model_visible_failure() { let completion = failure(&tool("OldTool")); assert!(completion.result().is_error); assert!(completion.result().content.contains("not available")); assert_eq!( completion .tool_call() .tool .as_ref() .and_then(|tool| match tool { pb::tool_call::Tool::McpToolCall(tool) => tool.args.as_ref(), _ => None, }) .map(|args| args.tool_name.as_str()), Some("OldTool") ); } }