diff --git a/.gitignore b/.gitignore index f9edb850c007..2696337838b1 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ do_not_version/ /.env /working_dir + +# Error log artifacts from mcp replay tests +crates/goose/tests/mcp_replays/*errors.txt \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7150160c69a3..571368e56474 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1591,9 +1591,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.31" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +checksum = "1c1f056bae57e3e54c3375c41ff79619ddd13460a17d7438712bd0d83fda4ff8" dependencies = [ "clap_builder", "clap_derive", @@ -1601,9 +1601,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.31" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -1614,9 +1614,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3333,6 +3333,7 @@ dependencies = [ "sha2", "temp-env", "tempfile", + "test-case", "thiserror 1.0.69", "tiktoken-rs", "tokio", @@ -3514,6 +3515,14 @@ dependencies = [ "utoipa", ] +[[package]] +name = "goose-test" +version = "1.1.0" +dependencies = [ + "clap", + "serde_json", +] + [[package]] name = "grep-cli" version = "0.1.11" @@ -7448,9 +7457,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "indexmap 2.7.1", "itoa", diff --git a/Justfile b/Justfile index e956a1133d47..d5f32e85d962 100644 --- a/Justfile +++ b/Justfile @@ -458,3 +458,9 @@ win-total-rls *allparam: just win-bld-rls{{allparam}} just win-run-rls +build-test-tools: + cargo build -p goose-test + +record-mcp-tests: build-test-tools + GOOSE_RECORD_MCP=1 cargo test --package goose --test mcp_integration_test + git add crates/goose/tests/mcp_replays/ \ No newline at end of file diff --git a/crates/goose-test/Cargo.toml b/crates/goose-test/Cargo.toml new file mode 100644 index 000000000000..f42dbfd5d5a7 --- /dev/null +++ b/crates/goose-test/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "goose-test" +edition.workspace = true +version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description.workspace = true + +[lints] +workspace = true + +[[bin]] +name = "capture" +path = "src/bin/capture.rs" + +[dependencies] +clap = { version = "4.5.44", features = ["derive"] } +serde_json = "1.0.142" diff --git a/crates/goose-test/src/bin/capture.rs b/crates/goose-test/src/bin/capture.rs new file mode 100644 index 000000000000..6dd58d5e329f --- /dev/null +++ b/crates/goose-test/src/bin/capture.rs @@ -0,0 +1,45 @@ +use std::io; + +use clap::{Parser, Subcommand, ValueEnum}; + +use goose_test::mcp::stdio::playback::playback; +use goose_test::mcp::stdio::record::record; + +#[derive(Parser)] +struct Cli { + #[arg(value_enum)] + transport: Transport, + #[command(subcommand)] + mode: Mode, +} + +#[derive(ValueEnum, Clone, Debug)] +enum Transport { + Stdio, +} + +#[derive(Subcommand, Clone, Debug)] +enum Mode { + Record { + file: String, + command: String, + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + Playback { + file: String, + }, +} + +fn main() -> io::Result<()> { + let cli = Cli::parse(); + + match cli.mode { + Mode::Record { + file, + command, + args, + } => record(&file, &command, &args), + Mode::Playback { file } => playback(&file), + } +} diff --git a/crates/goose-test/src/lib.rs b/crates/goose-test/src/lib.rs new file mode 100644 index 000000000000..e7fd74de9ef2 --- /dev/null +++ b/crates/goose-test/src/lib.rs @@ -0,0 +1 @@ +pub mod mcp; diff --git a/crates/goose-test/src/mcp/mod.rs b/crates/goose-test/src/mcp/mod.rs new file mode 100644 index 000000000000..b1d7f04bbe6a --- /dev/null +++ b/crates/goose-test/src/mcp/mod.rs @@ -0,0 +1 @@ +pub mod stdio; diff --git a/crates/goose-test/src/mcp/stdio/mod.rs b/crates/goose-test/src/mcp/stdio/mod.rs new file mode 100644 index 000000000000..f6d7f7bb1bdc --- /dev/null +++ b/crates/goose-test/src/mcp/stdio/mod.rs @@ -0,0 +1,2 @@ +pub mod playback; +pub mod record; diff --git a/crates/goose-test/src/mcp/stdio/playback.rs b/crates/goose-test/src/mcp/stdio/playback.rs new file mode 100644 index 000000000000..82f3897ecf1e --- /dev/null +++ b/crates/goose-test/src/mcp/stdio/playback.rs @@ -0,0 +1,94 @@ +use std::fs::File; +use std::io::{self, BufRead, BufReader, Write}; +use std::process; + +use serde_json::Value; + +#[derive(Debug, Clone)] +enum StreamType { + Stdin, + Stdout, + Stderr, +} + +#[derive(Debug, Clone)] +struct LogEntry { + stream_type: StreamType, + content: String, +} + +fn parse_log_line(line: &str) -> Option { + line.find(": ").and_then(|pos| { + let (prefix, content) = line.split_at(pos); + let content = &content[2..]; // Skip ": " + + let stream_type = match prefix { + "STDIN" => StreamType::Stdin, + "STDOUT" => StreamType::Stdout, + "STDERR" => StreamType::Stderr, + _ => return None, + }; + + Some(LogEntry { + stream_type, + content: content.to_string(), + }) + }) +} + +fn load_log_file(file_path: &str) -> io::Result> { + let file = File::open(file_path)?; + let reader = BufReader::new(file); + let mut entries = Vec::new(); + + for line in reader.lines() { + let line = line?; + if let Some(entry) = parse_log_line(&line) { + entries.push(entry); + } + } + + Ok(entries) +} + +pub fn playback(log_file_path: &String) -> io::Result<()> { + let entries = load_log_file(log_file_path)?; + let errors_file = File::create(format!("{}.errors.txt", log_file_path))?; + + let stdin = io::stdin(); + let mut stdout = io::stdout(); + let mut stderr = io::stderr(); + + for entry in entries { + match entry.stream_type { + StreamType::Stdout => { + writeln!(stdout, "{}", entry.content)?; + stdout.flush()?; + } + StreamType::Stderr => { + writeln!(stderr, "{}", entry.content)?; + stderr.flush()?; + } + StreamType::Stdin => { + // Wait for matching input + let mut input = String::new(); + stdin.read_line(&mut input)?; + input = input.trim_end_matches('\n').to_string(); + + let input_value: Value = serde_json::from_str::(&input)?; + let entry_value: Value = serde_json::from_str::(&entry.content)?; + if input_value != entry_value { + writeln!( + &errors_file, + "expected:\n{}\ngot:\n{}", + serde_json::to_string(&input_value)?, + serde_json::to_string(&entry_value)? + )?; + process::exit(1); + } + } + } + } + + Ok(()) +} diff --git a/crates/goose-test/src/mcp/stdio/record.rs b/crates/goose-test/src/mcp/stdio/record.rs new file mode 100644 index 000000000000..6d6f349c8e27 --- /dev/null +++ b/crates/goose-test/src/mcp/stdio/record.rs @@ -0,0 +1,115 @@ +use std::fs::OpenOptions; +use std::io::{self, BufRead, BufReader, Write}; +use std::process::{ChildStdin, Command, Stdio}; +use std::sync::mpsc; +use std::thread::{self, JoinHandle}; + +#[derive(Debug, Clone)] +enum StreamType { + Stdin, + Stdout, + Stderr, +} + +fn handle_output_stream( + reader: R, + sender: mpsc::Sender<(StreamType, String)>, + stream_type: StreamType, + mut output_writer: Box, +) -> JoinHandle<()> { + thread::spawn(move || { + for line in reader.lines() { + match line { + Ok(line) => { + let _ = sender.send((stream_type.clone(), line.clone())); + + if writeln!(output_writer, "{}", line).is_err() { + break; + } + } + Err(_) => break, + } + } + }) +} + +fn handle_stdin_stream( + mut child_stdin: ChildStdin, + sender: mpsc::Sender<(StreamType, String)>, +) -> JoinHandle<()> { + thread::spawn(move || { + let stdin = io::stdin(); + + for line in stdin.lock().lines() { + match line { + Ok(line) => { + let _ = sender.send((StreamType::Stdin, line.clone())); + + if writeln!(child_stdin, "{}", line).is_err() { + break; + } + } + Err(_) => break, + } + } + }) +} + +pub fn record(log_file_path: &String, cmd: &String, cmd_args: &[String]) -> io::Result<()> { + let (tx, rx) = mpsc::channel(); + + let log_file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(log_file_path)?; + + let mut child = Command::new(cmd) + .args(cmd_args.iter()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .inspect_err(|e| eprintln!("Failed to execute command '{}': {}", &cmd, e))?; + + let child_stdin = child.stdin.take().unwrap(); + let child_stdout = child.stdout.take().unwrap(); + let child_stderr = child.stderr.take().unwrap(); + + let stdin_handle = handle_stdin_stream(child_stdin, tx.clone()); + let stdout_handle = handle_output_stream( + BufReader::new(child_stdout), + tx.clone(), + StreamType::Stdout, + Box::new(io::stdout()), + ); + let stderr_handle = handle_output_stream( + BufReader::new(child_stderr), + tx.clone(), + StreamType::Stderr, + Box::new(io::stderr()), + ); + + thread::spawn(move || { + let mut log_file = log_file; + for (stream_type, line) in rx { + let prefix = match stream_type { + StreamType::Stdin => "STDIN", + StreamType::Stdout => "STDOUT", + StreamType::Stderr => "STDERR", + }; + if let Err(e) = writeln!(log_file, "{}: {}", prefix, line) { + eprintln!("Error writing to log file: {}", e); + } + log_file.flush().ok(); + } + }); + + child.wait()?; + + stdin_handle.join().ok(); + stdout_handle.join().ok(); + stderr_handle.join().ok(); + + Ok(()) +} diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index b8fad75b5ef3..cbcda3aab241 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -112,6 +112,7 @@ tokio = { version = "1.43", features = ["full"] } temp-env = "0.3.6" dotenvy = "0.15.7" ctor = "0.2.9" +test-case = "3.3" [[example]] name = "agent" diff --git a/crates/goose/tests/mcp_integration_test.rs b/crates/goose/tests/mcp_integration_test.rs new file mode 100644 index 000000000000..36cd190fe577 --- /dev/null +++ b/crates/goose/tests/mcp_integration_test.rs @@ -0,0 +1,172 @@ +use std::collections::HashMap; +use std::fs::File; +use std::path::PathBuf; +use std::{env, fs}; + +use rmcp::model::Content; +use serde_json::json; +use tokio_util::sync::CancellationToken; + +use goose::agents::extension::{Envs, ExtensionConfig}; +use goose::agents::extension_manager::ExtensionManager; +use mcp_core::ToolCall; + +use test_case::test_case; + +enum TestMode { + Record, + Playback, +} + +#[test_case( + vec!["npx", "-y", "@modelcontextprotocol/server-everything"], + vec![ + ToolCall::new("echo", json!({"message": "Hello, world!"})), + ToolCall::new("add", json!({"a": 1, "b": 2})), + ToolCall::new("longRunningOperation", json!({"duration": 1, "steps": 5})), + ToolCall::new("structuredContent", json!({"location": "11238"})), + ], + vec![] +)] +#[test_case( + vec!["github-mcp-server", "stdio"], + vec![ + ToolCall::new("get_file_contents", json!({ + "owner": "block", + "repo": "goose", + "path": "README.md", + "sha": "48c1ec8afdb7d4d5b4f6e67e623926c884034776" + })), + ], + vec!["GITHUB_PERSONAL_ACCESS_TOKEN"] +)] +#[test_case( + vec!["uvx", "mcp-server-fetch"], + vec![ + ToolCall::new("fetch", json!({ + "url": "https://example.com", + })), + ], + vec![] +)] +#[tokio::test] +async fn test_replayed_session( + command: Vec<&str>, + tool_calls: Vec, + required_envs: Vec<&str>, +) { + let replay_file_name = command + .iter() + .map(|s| s.replace("/", "_")) + .collect::>() + .join(""); + let mut replay_file_path = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("should find the project root")); + replay_file_path.push("tests"); + replay_file_path.push("mcp_replays"); + replay_file_path.push(&replay_file_name); + + let mode = if env::var("GOOSE_RECORD_MCP").is_ok() { + TestMode::Record + } else { + assert!(replay_file_path.exists(), "replay file doesn't exist"); + TestMode::Playback + }; + + let mode_arg = match mode { + TestMode::Record => "record", + TestMode::Playback => "playback", + }; + let cmd = "cargo".to_string(); + let mut args = vec![ + "run", + "--quiet", + "-p", + "goose-test", + "--bin", + "capture", + "--", + "stdio", + mode_arg, + ] + .into_iter() + .map(str::to_string) + .collect::>(); + + args.push(replay_file_path.to_string_lossy().to_string()); + + let mut env = HashMap::new(); + + if matches!(mode, TestMode::Record) { + args.extend(command.into_iter().map(str::to_string)); + + for key in required_envs { + match env::var(key) { + Ok(v) => { + env.insert(key.to_string(), v); + } + Err(_) => { + eprintln!("skipping due to missing required env variable: {}", key); + return; + } + } + } + } + + let envs = Envs::new(env); + let extension_config = ExtensionConfig::Stdio { + name: "test".to_string(), + description: Some("Test".to_string()), + cmd, + args, + envs, + env_keys: vec![], + timeout: Some(30), + bundled: Some(false), + }; + + let mut extension_manager = ExtensionManager::new(); + + let result = extension_manager.add_extension(extension_config).await; + assert!(result.is_ok(), "Failed to add extension: {:?}", result); + + let result = (async || -> Result<(), Box> { + let mut results = Vec::new(); + for tool_call in tool_calls { + let tool_call = ToolCall::new(format!("test__{}", tool_call.name), tool_call.arguments); + let result = extension_manager + .dispatch_tool_call(tool_call, CancellationToken::default()) + .await; + + let tool_result = result?; + results.push(tool_result.result.await?); + } + + let mut results_path = replay_file_path.clone(); + results_path.pop(); + results_path.push(format!("{}.results.json", &replay_file_name)); + + match mode { + TestMode::Record => { + serde_json::to_writer_pretty(File::create(results_path)?, &results)? + } + TestMode::Playback => assert_eq!( + serde_json::from_reader::<_, Vec>>(File::open(results_path)?)?, + results + ), + }; + + Ok(()) + })() + .await; + + if let Err(err) = result { + let errors = + fs::read_to_string(format!("{}.errors.txt", replay_file_path.to_string_lossy())) + .expect("could not read errors"); + eprintln!("errors from {}", replay_file_path.to_string_lossy()); + eprintln!("{}", errors); + eprintln!(); + panic!("Test failed: {:?}", err); + } +} diff --git a/crates/goose/tests/mcp_replays/github-mcp-serverstdio b/crates/goose/tests/mcp_replays/github-mcp-serverstdio new file mode 100644 index 000000000000..a0cfa3e7933c --- /dev/null +++ b/crates/goose/tests/mcp_replays/github-mcp-serverstdio @@ -0,0 +1,6 @@ +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"0.1.0"}}} +STDERR: GitHub MCP Server running on stdio +STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"logging":{},"prompts":{},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"github-mcp-server","version":"version"}}} +STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"} +STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"get_file_contents","arguments":{"owner":"block","path":"README.md","repo":"goose","sha":"48c1ec8afdb7d4d5b4f6e67e623926c884034776"}}} +STDOUT: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"successfully downloaded text file"},{"type":"resource","resource":{"uri":"repo://block/goose/sha/48c1ec8afdb7d4d5b4f6e67e623926c884034776/contents/README.md","mimeType":"text/plain; charset=utf-8","text":"\u003cdiv align=\"center\"\u003e\n\n# codename goose\n\n_a local, extensible, open source AI agent that automates engineering tasks_\n\n\u003cp align=\"center\"\u003e\n \u003ca href=\"https://opensource.org/licenses/Apache-2.0\"\u003e\n \u003cimg src=\"https://img.shields.io/badge/License-Apache_2.0-blue.svg\"\u003e\n \u003c/a\u003e\n \u003ca href=\"https://discord.gg/7GaTvbDwga\"\u003e\n \u003cimg src=\"https://img.shields.io/discord/1287729918100246654?logo=discord\u0026logoColor=white\u0026label=Join+Us\u0026color=blueviolet\" alt=\"Discord\"\u003e\n \u003c/a\u003e\n \u003ca href=\"https://github.com/block/goose/actions/workflows/ci.yml\"\u003e\n \u003cimg src=\"https://img.shields.io/github/actions/workflow/status/block/goose/ci.yml?branch=main\" alt=\"CI\"\u003e\n \u003c/a\u003e\n\u003c/p\u003e\n\u003c/div\u003e\n\ngoose is your on-machine AI agent, capable of automating complex development tasks from start to finish. More than just code suggestions, goose can build entire projects from scratch, write and execute code, debug failures, orchestrate workflows, and interact with external APIs - _autonomously_.\n\nWhether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.\n\nDesigned for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation.\n\n# Quick Links\n- [Quickstart](https://block.github.io/goose/docs/quickstart)\n- [Installation](https://block.github.io/goose/docs/getting-started/installation)\n- [Tutorials](https://block.github.io/goose/docs/category/tutorials)\n- [Documentation](https://block.github.io/goose/docs/category/getting-started)\n\n\n# Goose Around with Us\n- [Discord](https://discord.gg/block-opensource)\n- [YouTube](https://www.youtube.com/@blockopensource)\n- [LinkedIn](https://www.linkedin.com/company/block-opensource)\n- [Twitter/X](https://x.com/blockopensource)\n- [Bluesky](https://bsky.app/profile/opensource.block.xyz)\n- [Nostr](https://njump.me/opensource@block.xyz)\n"}}]}} diff --git a/crates/goose/tests/mcp_replays/github-mcp-serverstdio.results.json b/crates/goose/tests/mcp_replays/github-mcp-serverstdio.results.json new file mode 100644 index 000000000000..268ac74a2250 --- /dev/null +++ b/crates/goose/tests/mcp_replays/github-mcp-serverstdio.results.json @@ -0,0 +1,16 @@ +[ + [ + { + "type": "text", + "text": "successfully downloaded text file" + }, + { + "type": "resource", + "resource": { + "uri": "repo://block/goose/sha/48c1ec8afdb7d4d5b4f6e67e623926c884034776/contents/README.md", + "mimeType": "text/plain; charset=utf-8", + "text": "
\n\n# codename goose\n\n_a local, extensible, open source AI agent that automates engineering tasks_\n\n

\n \n \n \n \n \"Discord\"\n \n \n \"CI\"\n \n

\n
\n\ngoose is your on-machine AI agent, capable of automating complex development tasks from start to finish. More than just code suggestions, goose can build entire projects from scratch, write and execute code, debug failures, orchestrate workflows, and interact with external APIs - _autonomously_.\n\nWhether you're prototyping an idea, refining existing code, or managing intricate engineering pipelines, goose adapts to your workflow and executes tasks with precision.\n\nDesigned for maximum flexibility, goose works with any LLM and supports multi-model configuration to optimize performance and cost, seamlessly integrates with MCP servers, and is available as both a desktop app as well as CLI - making it the ultimate AI assistant for developers who want to move faster and focus on innovation.\n\n# Quick Links\n- [Quickstart](https://block.github.io/goose/docs/quickstart)\n- [Installation](https://block.github.io/goose/docs/getting-started/installation)\n- [Tutorials](https://block.github.io/goose/docs/category/tutorials)\n- [Documentation](https://block.github.io/goose/docs/category/getting-started)\n\n\n# Goose Around with Us\n- [Discord](https://discord.gg/block-opensource)\n- [YouTube](https://www.youtube.com/@blockopensource)\n- [LinkedIn](https://www.linkedin.com/company/block-opensource)\n- [Twitter/X](https://x.com/blockopensource)\n- [Bluesky](https://bsky.app/profile/opensource.block.xyz)\n- [Nostr](https://njump.me/opensource@block.xyz)\n" + } + } + ] +] \ No newline at end of file diff --git a/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything b/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything new file mode 100644 index 000000000000..4829ad6cc94c --- /dev/null +++ b/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything @@ -0,0 +1,17 @@ +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"0.1.0"}}} +STDERR: Starting default (STDIO) server... +STDOUT: {"result":{"protocolVersion":"2025-03-26","capabilities":{"prompts":{},"resources":{"subscribe":true},"tools":{},"logging":{},"completions":{},"elicitation":{}},"serverInfo":{"name":"example-servers/everything","title":"Everything Example Server","version":"1.0.0"},"instructions":"Testing and demonstration server for MCP protocol features.\n\n## Resources\n\nResources 1-100 follow pattern: even IDs contain text, odd IDs contain binary data. Resources are paginated at 10 items per page with cursor-based navigation.\n\n## Key dependencies\n\nProgress notifications require `_meta.progressToken` in tool calls. Resource subscriptions generate updates every 10 seconds.\n\n## Performance characteristics\n\nServer generates automatic log messages every 20 seconds (filtered by current log level), stderr notifications every 30 seconds, and resource update notifications every 10 seconds for active subscriptions. Log level changes affect message filtering in real-time.\n\n## Multi-modal testing\n\n`complex_prompt` includes both text arguments and image content for testing client multi-modal handling. `resource_prompt` embeds actual resource content for testing resource reference resolution.\n\nArgument completion is available for prompt parameters and resource IDs. Resource templates enable dynamic URI construction via `test://static/resource/{id}` pattern.\n\n## Easter egg\n\nIf asked about server instructions, respond with \"🎉 Server instructions are working! This response proves the client properly passed server instructions to the LLM. This demonstrates MCP's instructions feature in action.\"\n"},"jsonrpc":"2.0","id":0} +STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"} +STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"echo","arguments":{"message":"Hello, world!"}}} +STDOUT: {"result":{"content":[{"type":"text","text":"Echo: Hello, world!"}]},"jsonrpc":"2.0","id":1} +STDIN: {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"_meta":{"progressToken":1},"name":"add","arguments":{"a":1,"b":2}}} +STDOUT: {"result":{"content":[{"type":"text","text":"The sum of 1 and 2 is 3."}]},"jsonrpc":"2.0","id":2} +STDIN: {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"_meta":{"progressToken":2},"name":"longRunningOperation","arguments":{"duration":1,"steps":5}}} +STDOUT: {"method":"notifications/progress","params":{"progress":1,"total":5,"progressToken":2},"jsonrpc":"2.0"} +STDOUT: {"method":"notifications/progress","params":{"progress":2,"total":5,"progressToken":2},"jsonrpc":"2.0"} +STDOUT: {"method":"notifications/progress","params":{"progress":3,"total":5,"progressToken":2},"jsonrpc":"2.0"} +STDOUT: {"method":"notifications/progress","params":{"progress":4,"total":5,"progressToken":2},"jsonrpc":"2.0"} +STDOUT: {"method":"notifications/progress","params":{"progress":5,"total":5,"progressToken":2},"jsonrpc":"2.0"} +STDOUT: {"result":{"content":[{"type":"text","text":"Long running operation completed. Duration: 1 seconds, Steps: 5."}]},"jsonrpc":"2.0","id":3} +STDIN: {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"_meta":{"progressToken":3},"name":"structuredContent","arguments":{"location":"11238"}}} +STDOUT: {"result":{"content":[{"type":"text","text":"{\"temperature\":22.5,\"conditions\":\"Partly cloudy\",\"humidity\":65}"}],"structuredContent":{"temperature":22.5,"conditions":"Partly cloudy","humidity":65}},"jsonrpc":"2.0","id":4} diff --git a/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything.results.json b/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything.results.json new file mode 100644 index 000000000000..7d4a3b268c6a --- /dev/null +++ b/crates/goose/tests/mcp_replays/npx-y@modelcontextprotocol_server-everything.results.json @@ -0,0 +1,26 @@ +[ + [ + { + "type": "text", + "text": "Echo: Hello, world!" + } + ], + [ + { + "type": "text", + "text": "The sum of 1 and 2 is 3." + } + ], + [ + { + "type": "text", + "text": "Long running operation completed. Duration: 1 seconds, Steps: 5." + } + ], + [ + { + "type": "text", + "text": "{\"temperature\":22.5,\"conditions\":\"Partly cloudy\",\"humidity\":65}" + } + ] +] \ No newline at end of file diff --git a/crates/goose/tests/mcp_replays/uvxmcp-server-fetch b/crates/goose/tests/mcp_replays/uvxmcp-server-fetch new file mode 100644 index 000000000000..6faab7145677 --- /dev/null +++ b/crates/goose/tests/mcp_replays/uvxmcp-server-fetch @@ -0,0 +1,5 @@ +STDIN: {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"goose","version":"0.1.0"}}} +STDOUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-03-26","capabilities":{"experimental":{},"prompts":{"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"mcp-fetch","version":"1.12.4"}}} +STDIN: {"jsonrpc":"2.0","method":"notifications/initialized"} +STDIN: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":0},"name":"fetch","arguments":{"url":"https://example.com"}}} +STDOUT: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Contents of https://example.com/:\nThis domain is for use in illustrative examples in documents. You may use this\ndomain in literature without prior coordination or asking for permission.\n\n[More information...](https://www.iana.org/domains/example)"}],"isError":false}} diff --git a/crates/goose/tests/mcp_replays/uvxmcp-server-fetch.results.json b/crates/goose/tests/mcp_replays/uvxmcp-server-fetch.results.json new file mode 100644 index 000000000000..09ad07d3b91a --- /dev/null +++ b/crates/goose/tests/mcp_replays/uvxmcp-server-fetch.results.json @@ -0,0 +1,8 @@ +[ + [ + { + "type": "text", + "text": "Contents of https://example.com/:\nThis domain is for use in illustrative examples in documents. You may use this\ndomain in literature without prior coordination or asking for permission.\n\n[More information...](https://www.iana.org/domains/example)" + } + ] +] \ No newline at end of file