mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(dev-mcp): remove buzz_send_message tool in favor of buzz CLI send path (#2043)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent
4be5c6ae32
commit
a30d525155
@@ -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 <UUID> --since <ts>`.
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
state: &SharedState,
|
||||
params: SendMessageParams,
|
||||
) -> Result<CallToolResult, ErrorData> {
|
||||
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<PathBuf> {
|
||||
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::<Vec<_>>(),
|
||||
[
|
||||
"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));
|
||||
}
|
||||
}
|
||||
@@ -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<buzz_message::SendMessageParams>,
|
||||
) -> Result<CallToolResult, ErrorData> {
|
||||
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."
|
||||
|
||||
Reference in New Issue
Block a user