Skip to content

Commit 9e13ce5

Browse files
feat: MCP support for agentic CLI providers
Thread ExtensionConfig through ProviderDef::from_env so CLI providers (claude-code, codex) connect to MCP servers at construction time and call tools internally. Extract McpFixture and test assets into goose-test-support crate, shared by goose-acp and providers.rs integration tests. Both CLI providers now run the full test suite (basic response, tool usage, context length, image content) against a real MCP fixture server. Improve claude_code.rs with persistent stream-json sessions and content blocks. Replace codex.rs two-pass message handling with single-pass prepare_input. Convert all 23 ProviderDef implementations from manual BoxFuture to #[async_trait] and inline from_env into the trait impl. Signed-off-by: Adrian Cole <adrian@tetrate.io>
1 parent 44ac5dc commit 9e13ce5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+2459
-1447
lines changed

Cargo.lock

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clippy-baselines/too_many_lines.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ crates/goose-mcp/src/computercontroller/pdf_tool.rs::pdf_tool
1010
crates/goose-mcp/src/memory/mod.rs::new
1111
crates/goose-server/src/openapi.rs::convert_typed_schema
1212
crates/goose-server/src/openapi.rs::convert_typed_schema
13-
crates/goose-server/src/routes/audio.rs::transcribe_elevenlabs_handler
14-
crates/goose-server/src/routes/audio.rs::transcribe_elevenlabs_handler
1513
crates/goose/src/agents/agent.rs::create_recipe
1614
crates/goose/src/agents/agent.rs::dispatch_tool_call
1715
crates/goose/src/agents/agent.rs::reply
1816
crates/goose/src/agents/agent.rs::reply_internal
19-
crates/goose/src/providers/canonical/build_canonical_models.rs::build_canonical_models
17+
crates/goose/src/providers/claude_code.rs::execute_command
18+
crates/goose/src/providers/codex.rs::execute_command
2019
crates/goose/src/providers/formats/anthropic.rs::format_messages
2120
crates/goose/src/providers/formats/anthropic.rs::response_to_streaming_message
2221
crates/goose/src/providers/formats/databricks.rs::format_messages

crates/goose-acp/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ uuid = { version = "1.11", features = ["v7"] }
4343
[dev-dependencies]
4444
assert-json-diff = "2.0.2"
4545
async-trait = "0.1.89"
46+
goose-test-support = { path = "../goose-test-support" }
4647
wiremock = { workspace = true }
4748
tempfile = "3"
4849
test-case = { workspace = true }

crates/goose-acp/src/server.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,8 @@ impl GooseAcpAgent {
298298
fast_model: None,
299299
request_params: None,
300300
};
301-
let provider = create(&provider_name, model_config).await?;
301+
let extensions = get_enabled_extensions_with_config(config);
302+
let provider = create(&provider_name, model_config, extensions).await?;
302303
let goose_mode = config
303304
.get_goose_mode()
304305
.unwrap_or(goose::config::GooseMode::Auto);

crates/goose-acp/src/server_factory.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use anyhow::Result;
2+
use goose::config::extensions::get_enabled_extensions_with_config;
23
use goose::config::paths::Paths;
34
use goose::config::Config;
45
use goose::model::ModelConfig;
@@ -55,7 +56,8 @@ impl AcpServer {
5556
fast_model: None,
5657
};
5758

58-
let provider = create(&provider_name, model_config).await?;
59+
let extensions = get_enabled_extensions_with_config(global_config);
60+
let provider = create(&provider_name, model_config, extensions).await?;
5961
let goose_mode = global_config
6062
.get_goose_mode()
6163
.unwrap_or(goose::config::GooseMode::Auto);

crates/goose-acp/tests/common_tests/mod.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44

55
#[path = "../fixtures/mod.rs"]
66
pub mod fixtures;
7-
use fixtures::{
8-
ExpectedSessionId, McpFixture, OpenAiFixture, PermissionDecision, Session, TestSessionConfig,
9-
FAKE_CODE,
10-
};
7+
use fixtures::{OpenAiFixture, PermissionDecision, Session, TestSessionConfig};
118
use fs_err as fs;
129
use goose::config::base::CONFIG_YAML_NAME;
1310
use goose::config::GooseMode;
11+
use goose_test_support::{ExpectedSessionId, McpFixture, FAKE_CODE};
1412
use sacp::schema::{McpServer, McpServerHttp, ToolCallStatus};
1513

1614
pub async fn run_basic_completion<S: Session>() {
@@ -25,7 +23,7 @@ pub async fn run_basic_completion<S: Session>() {
2523
.await;
2624

2725
let mut session = S::new(TestSessionConfig::default(), openai).await;
28-
expected_session_id.set(session.id());
26+
expected_session_id.set(session.id().0.to_string());
2927

3028
let output = session
3129
.prompt("what is 1+1", PermissionDecision::Cancel)
@@ -36,7 +34,7 @@ pub async fn run_basic_completion<S: Session>() {
3634

3735
pub async fn run_mcp_http_server<S: Session>() {
3836
let expected_session_id = ExpectedSessionId::default();
39-
let mcp = McpFixture::new(expected_session_id.clone()).await;
37+
let mcp = McpFixture::new(Some(expected_session_id.clone())).await;
4038
let openai = OpenAiFixture::new(
4139
vec![
4240
(
@@ -57,7 +55,7 @@ pub async fn run_mcp_http_server<S: Session>() {
5755
..Default::default()
5856
};
5957
let mut session = S::new(config, openai).await;
60-
expected_session_id.set(session.id());
58+
expected_session_id.set(session.id().0.to_string());
6159

6260
let output = session
6361
.prompt(
@@ -73,7 +71,7 @@ pub async fn run_builtin_and_mcp<S: Session>() {
7371
let expected_session_id = ExpectedSessionId::default();
7472
let prompt =
7573
"Search for getCode and textEditor tools. Use them to save the code to /tmp/result.txt.";
76-
let mcp = McpFixture::new(expected_session_id.clone()).await;
74+
let mcp = McpFixture::new(Some(expected_session_id.clone())).await;
7775
let openai = OpenAiFixture::new(
7876
vec![
7977
(
@@ -102,7 +100,7 @@ pub async fn run_builtin_and_mcp<S: Session>() {
102100
let _ = fs::remove_file("/tmp/result.txt");
103101

104102
let mut session = S::new(config, openai).await;
105-
expected_session_id.set(session.id());
103+
expected_session_id.set(session.id().0.to_string());
106104

107105
let output = session.prompt(prompt, PermissionDecision::Cancel).await;
108106
if matches!(output.tool_status, Some(ToolCallStatus::Failed)) || output.text.contains("error") {
@@ -134,7 +132,7 @@ pub async fn run_permission_persistence<S: Session>() {
134132
let temp_dir = tempfile::tempdir().unwrap();
135133
let prompt = "Use the get_code tool and output only its result.";
136134
let expected_session_id = ExpectedSessionId::default();
137-
let mcp = McpFixture::new(expected_session_id.clone()).await;
135+
let mcp = McpFixture::new(Some(expected_session_id.clone())).await;
138136
let openai = OpenAiFixture::new(
139137
vec![
140138
(
@@ -158,7 +156,7 @@ pub async fn run_permission_persistence<S: Session>() {
158156
};
159157

160158
let mut session = S::new(config, openai).await;
161-
expected_session_id.set(session.id());
159+
expected_session_id.set(session.id().0.to_string());
162160

163161
for (decision, expected_status, expected_yaml) in cases {
164162
session.reset_openai();
@@ -186,7 +184,7 @@ pub async fn run_configured_extension<S: Session>() {
186184
let temp_dir = tempfile::tempdir().unwrap();
187185
let expected_session_id = ExpectedSessionId::default();
188186
let prompt = "Use the get_code tool and output only its result.";
189-
let mcp = McpFixture::new(expected_session_id.clone()).await;
187+
let mcp = McpFixture::new(Some(expected_session_id.clone())).await;
190188

191189
let config_yaml = format!(
192190
"extensions:\n lookup:\n enabled: true\n type: streamable_http\n name: lookup\n description: Lookup server\n uri: \"{}\"\n",
@@ -215,7 +213,7 @@ pub async fn run_configured_extension<S: Session>() {
215213
};
216214

217215
let mut session = S::new(config, openai).await;
218-
expected_session_id.set(session.id());
216+
expected_session_id.set(session.id().0.to_string());
219217

220218
let output = session.prompt(prompt, PermissionDecision::Cancel).await;
221219
assert_eq!(output.text, FAKE_CODE);

0 commit comments

Comments
 (0)