Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b47f251
Handle MCP server notification messages
jamadeo May 19, 2025
0cca95c
Print something sensible
jamadeo May 19, 2025
42ec8a3
Fix mcp maybe
jamadeo May 20, 2025
8262134
It sends to the UI
jamadeo May 20, 2025
5212bbf
Push from MCP, but it doesn't quite work yet
jamadeo May 20, 2025
db585e2
Developer shell sends to the FE
jamadeo May 20, 2025
6dccbc6
Wire it up
jamadeo May 20, 2025
faca44e
Really make it work in the UI
jamadeo May 21, 2025
9e361d3
Merge remote-tracking branch 'origin/main' into jackamadeo/notifications
jamadeo May 21, 2025
28fa391
Fix several tests
jamadeo May 21, 2025
3f4021f
Move the CLI output to the right place
jamadeo May 21, 2025
6fb09c4
Remove a couple of logs
jamadeo May 21, 2025
8fa47e3
Add back import
jamadeo May 21, 2025
a9c3d80
Fix lint
jamadeo May 21, 2025
1ebfec3
Now fix more lint
jamadeo May 21, 2025
1f4b13d
Merge branch 'main' into jackamadeo/notifications
michaelneale May 21, 2025
31f7a5d
Merge branch 'main' into jackamadeo/notifications
michaelneale May 22, 2025
26a70cf
Merge branch 'main' into jackamadeo/notifications
michaelneale May 22, 2025
c790b0f
Better CLI render
jamadeo May 22, 2025
424e1fd
Spinner-style (cli)
jamadeo May 22, 2025
a9126b3
One less empty line
jamadeo May 22, 2025
1aceffa
Do something similar for the GUI
jamadeo May 22, 2025
d38ac17
Merge remote-tracking branch 'origin/main' into jackamadeo/notifications
jamadeo May 27, 2025
aacc0cb
Merge branch 'main' into jackamadeo/notifications
jamadeo May 27, 2025
19cb708
cleaner error handling for joined task
jamadeo May 29, 2025
f60a515
Merge remote-tracking branch 'origin/main' into re-merge
jamadeo May 30, 2025
82921d7
hang up
jamadeo May 30, 2025
9aadadd
Fix for a merge
jamadeo May 30, 2025
b896dee
Integration test
jamadeo May 30, 2025
a21973f
Fix max-height and expansion
jamadeo May 30, 2025
5d30e1b
Merge branch 'main' into jackamadeo/notifications
jamadeo May 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/goose-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ regex = "1.11.1"
minijinja = "2.8.0"
nix = { version = "0.30.1", features = ["process", "signal"] }
tar = "0.4"
indicatif = "0.17.11"

[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["wincred"] }
Expand Down
54 changes: 53 additions & 1 deletion crates/goose-cli/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod thinking;

pub use builder::{build_session, SessionBuilderConfig};
use console::Color;
use goose::agents::AgentEvent;
use goose::permission::permission_confirmation::PrincipalType;
use goose::permission::Permission;
use goose::permission::PermissionConfirmation;
Expand All @@ -26,6 +27,8 @@ use input::InputResult;
use mcp_core::handler::ToolError;
use mcp_core::prompt::PromptMessage;

use mcp_core::protocol::JsonRpcMessage;
use mcp_core::protocol::JsonRpcNotification;
use rand::{distributions::Alphanumeric, Rng};
use serde_json::Value;
use std::collections::HashMap;
Expand Down Expand Up @@ -713,12 +716,15 @@ impl Session {
)
.await?;

let mut progress_bars = output::McpSpinners::new();

use futures::StreamExt;
loop {
tokio::select! {
result = stream.next() => {
let _ = progress_bars.hide();
match result {
Some(Ok(message)) => {
Some(Ok(AgentEvent::Message(message))) => {
// If it's a confirmation request, get approval but otherwise do not render/persist
if let Some(MessageContent::ToolConfirmationRequest(confirmation)) = message.content.first() {
output::hide_thinking();
Expand Down Expand Up @@ -846,6 +852,51 @@ impl Session {
if interactive {output::show_thinking()};
}
}
Some(Ok(AgentEvent::McpNotification((_id, message)))) => {
if let JsonRpcMessage::Notification(JsonRpcNotification{
method,
params: Some(Value::Object(o)),
..
}) = message {
match method.as_str() {
"notifications/message" => {
let data = o.get("data").unwrap_or(&Value::Null);
let message = match data {
Value::String(s) => s.clone(),
Value::Object(o) => {
if let Some(Value::String(output)) = o.get("output") {
output.to_owned()
} else {
data.to_string()
}
},
v => {
v.to_string()
},
};
// output::render_text_no_newlines(&message, None, true);
progress_bars.log(&message);
},
"notifications/progress" => {
let progress = o.get("progress").and_then(|v| v.as_f64());
let token = o.get("progressToken").map(|v| v.to_string());
let message = o.get("message").and_then(|v| v.as_str());
let total = o
.get("total")
.and_then(|v| v.as_f64());
if let (Some(progress), Some(token)) = (progress, token) {
progress_bars.update(
token.as_str(),
progress,
total,
message,
);
}
},
_ => (),
}
}
}
Some(Err(e)) => {
eprintln!("Error: {}", e);
drop(stream);
Expand All @@ -872,6 +923,7 @@ impl Session {
}
}
}

Ok(())
}

Expand Down
68 changes: 66 additions & 2 deletions crates/goose-cli/src/session/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ use bat::WrappingMode;
use console::{style, Color};
use goose::config::Config;
use goose::message::{Message, MessageContent, ToolRequest, ToolResponse};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use mcp_core::prompt::PromptArgument;
use mcp_core::tool::ToolCall;
use serde_json::Value;
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::Error;
use std::path::Path;
use std::time::Duration;

// Re-export theme for use in main
#[derive(Clone, Copy)]
Expand Down Expand Up @@ -144,6 +147,10 @@ pub fn render_message(message: &Message, debug: bool) {
}

pub fn render_text(text: &str, color: Option<Color>, dim: bool) {
render_text_no_newlines(format!("\n{}\n\n", text).as_str(), color, dim);
}

pub fn render_text_no_newlines(text: &str, color: Option<Color>, dim: bool) {
let mut styled_text = style(text);
if dim {
styled_text = styled_text.dim();
Expand All @@ -153,7 +160,7 @@ pub fn render_text(text: &str, color: Option<Color>, dim: bool) {
} else {
styled_text = styled_text.green();
}
println!("\n{}\n", styled_text);
print!("{}", styled_text);
}

pub fn render_enter_plan_mode() {
Expand Down Expand Up @@ -359,7 +366,6 @@ fn render_shell_request(call: &ToolCall, debug: bool) {
}
_ => print_params(&call.arguments, 0, debug),
}
println!();
}

fn render_default_request(call: &ToolCall, debug: bool) {
Expand Down Expand Up @@ -568,6 +574,64 @@ pub fn display_greeting() {
println!("\nGoose is running! Enter your instructions, or try asking what goose can do.\n");
}

pub struct McpSpinners {
bars: HashMap<String, ProgressBar>,
log_spinner: Option<ProgressBar>,

multi_bar: MultiProgress,
}

impl McpSpinners {
pub fn new() -> Self {
McpSpinners {
bars: HashMap::new(),
log_spinner: None,
multi_bar: MultiProgress::new(),
}
}

pub fn log(&mut self, message: &str) {
let spinner = self.log_spinner.get_or_insert_with(|| {
let bar = self.multi_bar.add(
ProgressBar::new_spinner()
.with_style(
ProgressStyle::with_template("{spinner:.green} {msg}")
.unwrap()
.tick_chars("⠋⠙⠚⠛⠓⠒⠊⠉"),
)
.with_message(message.to_string()),
);
bar.enable_steady_tick(Duration::from_millis(100));
bar
});

spinner.set_message(message.to_string());
}

pub fn update(&mut self, token: &str, value: f64, total: Option<f64>, message: Option<&str>) {
let bar = self.bars.entry(token.to_string()).or_insert_with(|| {
if let Some(total) = total {
self.multi_bar.add(
ProgressBar::new((total * 100.0) as u64).with_style(
ProgressStyle::with_template("[{elapsed}] {bar:40} {pos:>3}/{len:3} {msg}")
.unwrap(),
),
)
} else {
self.multi_bar.add(ProgressBar::new_spinner())
}
});
bar.set_position((value * 100.0) as u64);
if let Some(msg) = message {
bar.set_message(msg.to_string());
}
}

pub fn hide(&mut self) -> Result<(), Error> {
self.multi_bar.clear()
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
7 changes: 5 additions & 2 deletions crates/goose-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::ptr;
use std::sync::Arc;

use futures::StreamExt;
use goose::agents::Agent;
use goose::agents::{Agent, AgentEvent};
use goose::message::Message;
use goose::model::ModelConfig;
use goose::providers::databricks::DatabricksProvider;
Expand Down Expand Up @@ -256,13 +256,16 @@ pub unsafe extern "C" fn goose_agent_send_message(

while let Some(message_result) = stream.next().await {
match message_result {
Ok(message) => {
Ok(AgentEvent::Message(message)) => {
// Get text or serialize to JSON
// Note: Message doesn't have as_text method, we'll serialize to JSON
if let Ok(json) = serde_json::to_string(&message) {
full_response.push_str(&json);
}
}
Ok(AgentEvent::McpNotification(_)) => {
// TODO: Handle MCP notifications.
}
Err(e) => {
full_response.push_str(&format!("\nError in message stream: {}", e));
}
Expand Down
5 changes: 3 additions & 2 deletions crates/goose-mcp/src/computercontroller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ use serde_json::{json, Value};
use std::{
collections::HashMap, fs, future::Future, path::PathBuf, pin::Pin, sync::Arc, sync::Mutex,
};
use tokio::process::Command;
use tokio::{process::Command, sync::mpsc};

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

use mcp_core::{
handler::{PromptError, ResourceError, ToolError},
prompt::Prompt,
protocol::ServerCapabilities,
protocol::{JsonRpcMessage, ServerCapabilities},
resource::Resource,
tool::{Tool, ToolAnnotations},
Content,
Expand Down Expand Up @@ -1155,6 +1155,7 @@ impl Router for ComputerControllerRouter {
&self,
tool_name: &str,
arguments: Value,
_notifier: mpsc::Sender<JsonRpcMessage>,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>> {
let this = self.clone();
let tool_name = tool_name.to_string();
Expand Down
Loading
Loading