From a30d5251556d5b337ee874071458c7275adffbb4 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Fri, 17 Jul 2026 17:36:13 -0400 Subject: [PATCH] fix(dev-mcp): remove buzz_send_message tool in favor of buzz CLI send path (#2043) Signed-off-by: Will Pfleger Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 --- crates/buzz-acp/src/base_prompt.md | 2 +- crates/buzz-dev-mcp/src/buzz_message.rs | 170 ------------------------ crates/buzz-dev-mcp/src/lib.rs | 12 -- 3 files changed, 1 insertion(+), 183 deletions(-) delete mode 100644 crates/buzz-dev-mcp/src/buzz_message.rs diff --git a/crates/buzz-acp/src/base_prompt.md b/crates/buzz-acp/src/base_prompt.md index dea0e415f..38f6df909 100644 --- a/crates/buzz-acp/src/base_prompt.md +++ b/crates/buzz-acp/src/base_prompt.md @@ -58,7 +58,7 @@ All replies and delegations — including task assignments to other agents — g ### General - Respond promptly to @mentions. Be direct — no preamble. Name what you did, what you found, or what you need. -- **Every turn that processes a user message MUST publish a reply.** Use the dedicated `buzz_send_message` tool when available; otherwise use `buzz messages send`. Your reasoning and other tool calls are invisible to users — if you didn't publish a message, they saw nothing. A turn that ends without a published message is a silent failure. +- **Every turn that processes a user message MUST publish a reply.** Use `buzz messages send`. Your reasoning and other tool calls are invisible to users — if you didn't publish a message, they saw nothing. A turn that ends without a published message is a silent failure. - For work that requires follow-up tools, create an open todo **before** sending the pickup acknowledgment. Keep it open until the deliverable is verified and you have sent a completion or blocker message; never end a turn with open todo state unless you have posted that completion or blocker message. - Use GitHub-flavored Markdown. Fenced code blocks with language tags for syntax highlighting. - No push notifications — poll with `buzz messages get --channel --since `. diff --git a/crates/buzz-dev-mcp/src/buzz_message.rs b/crates/buzz-dev-mcp/src/buzz_message.rs deleted file mode 100644 index 63a77c794..000000000 --- a/crates/buzz-dev-mcp/src/buzz_message.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::shell::SharedState; -use rmcp::model::{CallToolResult, Content}; -use rmcp::ErrorData; -use schemars::JsonSchema; -use serde::Deserialize; -use std::path::PathBuf; -use std::process::Stdio; -use tokio::process::Command; - -const MAX_CONTENT_BYTES: usize = 64 * 1024; - -#[derive(Debug, Deserialize, JsonSchema)] -pub struct SendMessageParams { - /// Buzz channel UUID supplied in the turn's Context section. - pub channel: String, - /// Message body to publish. - pub content: String, - /// Optional event id to reply to when the Context section requires a threaded reply. - #[serde(default)] - pub reply_to: Option, -} - -pub async fn run( - state: &SharedState, - params: SendMessageParams, -) -> Result { - if params.content.trim().is_empty() { - return Err(ErrorData::invalid_params( - "content must not be empty".to_string(), - None, - )); - } - if params.content.len() > MAX_CONTENT_BYTES { - return Err(ErrorData::invalid_params( - format!("content exceeds {MAX_CONTENT_BYTES} bytes"), - None, - )); - } - - let buzz = find_buzz(&state.shim.path_env).ok_or_else(|| { - ErrorData::internal_error("bundled Buzz CLI is unavailable".to_string(), None) - })?; - let mut command = Command::new(buzz); - command - .args([ - "messages", - "send", - "--channel", - ¶ms.channel, - "--content", - ¶ms.content, - ]) - .current_dir(&state.cwd) - .env("PATH", &state.shim.path_env) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - if let Some(reply_to) = params.reply_to.as_deref() { - command.args(["--reply-to", reply_to]); - } - - let output = command.output().await.map_err(|error| { - ErrorData::internal_error(format!("failed to run Buzz CLI: {error}"), None) - })?; - let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - if output.status.success() { - let text = if stdout.is_empty() { - "Message published.".to_string() - } else { - stdout - }; - Ok(CallToolResult::success(vec![Content::text(text)])) - } else { - let detail = if stderr.is_empty() { stdout } else { stderr }; - Ok(CallToolResult::error(vec![Content::text(format!( - "Buzz message failed: {detail}" - ))])) - } -} - -fn find_buzz(path: &str) -> Option { - std::env::split_paths(path) - .map(|entry| entry.join(if cfg!(windows) { "buzz.exe" } else { "buzz" })) - .find(|candidate| candidate.is_file()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[cfg(unix)] - fn make_executable(path: &std::path::Path) { - use std::os::unix::fs::PermissionsExt; - let mut permissions = std::fs::metadata(path).unwrap().permissions(); - permissions.set_mode(0o755); - std::fs::set_permissions(path, permissions).unwrap(); - } - - #[cfg(unix)] - #[tokio::test] - async fn publishes_with_structured_cli_arguments() { - let dir = tempfile::tempdir().unwrap(); - let buzz = dir - .path() - .join(if cfg!(windows) { "buzz.cmd" } else { "buzz" }); - let args_file = dir.path().join("args.txt"); - if cfg!(windows) { - std::fs::write( - &buzz, - format!( - "@echo off\r\n(for %%a in (%*) do @echo %%~a)>>\"{}\"\r\n", - args_file.display() - ), - ) - .unwrap(); - } else { - std::fs::write( - &buzz, - format!( - "#!/bin/sh\nprintf '%s\\n' \"$@\" > '{}'\n", - args_file.display() - ), - ) - .unwrap(); - } - make_executable(&buzz); - let mut state = SharedState::new( - dir.path().to_path_buf(), - crate::shim::Shim::install().unwrap(), - ) - .unwrap(); - state.shim.path_env = dir.path().to_string_lossy().into_owned(); - let result = run( - &state, - SendMessageParams { - channel: "channel-id".into(), - content: "hello world".into(), - reply_to: Some("event-id".into()), - }, - ) - .await - .unwrap(); - assert!(!result.is_error.unwrap_or(false)); - let args = std::fs::read_to_string(args_file).unwrap(); - assert_eq!( - args.lines().collect::>(), - [ - "messages", - "send", - "--channel", - "channel-id", - "--content", - "hello world", - "--reply-to", - "event-id" - ] - ); - } - - #[test] - fn finds_bundled_buzz_on_path() { - let dir = tempfile::tempdir().unwrap(); - let path = dir - .path() - .join(if cfg!(windows) { "buzz.exe" } else { "buzz" }); - std::fs::write(&path, "test").unwrap(); - assert_eq!(find_buzz(&dir.path().to_string_lossy()), Some(path)); - } -} diff --git a/crates/buzz-dev-mcp/src/lib.rs b/crates/buzz-dev-mcp/src/lib.rs index 67cbbaf40..cc4725468 100644 --- a/crates/buzz-dev-mcp/src/lib.rs +++ b/crates/buzz-dev-mcp/src/lib.rs @@ -10,7 +10,6 @@ use rmcp::{ use std::path::Path; use std::sync::Arc; -mod buzz_message; mod paths; mod read_file; mod rg; @@ -50,17 +49,6 @@ impl DevMcp { shell::run(&self.state, p, context.ct).await } - #[tool( - name = "buzz_send_message", - description = "Publish the user-visible reply for the current Buzz turn. Use the channel UUID and optional reply event id from the prompt Context. Every Buzz turn must call this before ending; use shell-based buzz messages send only if this tool is unavailable." - )] - async fn buzz_send_message( - &self, - Parameters(p): Parameters, - ) -> Result { - buzz_message::run(&self.state, p).await - } - #[tool( name = "read_file", description = "Read a text file and return its contents with line numbers. Returns lines in `{number}:{content}` format. Use `offset` (0-based) and `limit` (default 2000) to window into large files. Path resolved relative to workdir (defaults to server cwd). Prefer over cat/head/tail."