Skip to content

Commit 2ec3875

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 17dff13 commit 2ec3875

Some content is hidden

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

55 files changed

+1495
-945
lines changed

Cargo.lock

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

crates/goose-acp/src/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ impl GooseAcpAgent {
724724
let config = Config::new(&config_path, "goose")?;
725725
let model_id = config.get_goose_model()?;
726726
let model_config = goose::model::ModelConfig::new(&model_id)?;
727-
let provider = (self.provider_factory)(model_config).await?;
727+
let provider = (self.provider_factory)(model_config, Vec::new()).await?;
728728
self.agent.update_provider(provider, &session.id).await?;
729729
Ok(model_id)
730730
}

crates/goose-acp/src/server_factory.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ impl AcpServer {
3333
let disable_session_naming = config.get_goose_disable_session_naming().unwrap_or(false);
3434

3535
let config_dir = self.config.config_dir.clone();
36-
let provider_factory: ProviderConstructor = Arc::new(move |model_config| {
36+
let provider_factory: ProviderConstructor = Arc::new(move |model_config, extensions| {
3737
let config_dir = config_dir.clone();
3838
Box::pin(async move {
3939
let config_path = config_dir.join(goose::config::base::CONFIG_YAML_NAME);
4040
let config = goose::config::Config::new(&config_path, "goose")?;
4141
let provider_name = config
4242
.get_goose_provider()
4343
.map_err(|_| anyhow::anyhow!("No provider configured"))?;
44-
goose::providers::create(&provider_name, model_config).await
44+
goose::providers::create(&provider_name, model_config, extensions).await
4545
})
4646
});
4747

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ pub async fn spawn_acp_server_in_process(
191191
fs::write(&config_path, "GOOSE_MODEL: gpt-5-nano\n").unwrap();
192192
}
193193
let base_url = openai_base_url.to_string();
194-
let provider_factory: ProviderConstructor = Arc::new(move |model_config| {
194+
let provider_factory: ProviderConstructor = Arc::new(move |model_config, _extensions| {
195195
let base_url = base_url.clone();
196196
Box::pin(async move {
197197
let api_client =

crates/goose-acp/tests/server_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ fn test_initialize_without_provider() {
4747
let temp_dir = tempfile::tempdir().unwrap();
4848

4949
let provider_factory: ProviderConstructor =
50-
Arc::new(|_| Box::pin(async { Err(anyhow::anyhow!("no provider configured")) }));
50+
Arc::new(|_, _| Box::pin(async { Err(anyhow::anyhow!("no provider configured")) }));
5151

5252
let agent = Arc::new(
5353
GooseAcpAgent::new(

crates/goose-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ indicatif = "0.18.1"
5353
tokio-util = { version = "0.7.15", features = ["compat", "rt"] }
5454
anstream = "0.6.18"
5555
open = "5.3.2"
56+
url = { workspace = true }
5657
urlencoding = "2.1"
5758
clap_complete = "4.5.62"
5859

crates/goose-cli/src/commands/configure.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ async fn handle_oauth_configuration(provider_name: &str, key_name: &str) -> anyh
328328

329329
// Create a temporary provider instance to handle OAuth
330330
let temp_model = ModelConfig::new("temp")?;
331-
match create(provider_name, temp_model).await {
331+
match create(provider_name, temp_model, Vec::new()).await {
332332
Ok(provider) => match provider.configure_oauth().await {
333333
Ok(_) => {
334334
let _ = cliclack::log::success("OAuth authentication completed successfully!");
@@ -683,7 +683,7 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
683683
spin.start("Attempting to fetch supported models...");
684684
let models_res = {
685685
let temp_model_config = ModelConfig::new(&provider_meta.default_model)?;
686-
let temp_provider = create(provider_name, temp_model_config).await?;
686+
let temp_provider = create(provider_name, temp_model_config, Vec::new()).await?;
687687
retry_operation(&RetryConfig::default(), || async {
688688
temp_provider.fetch_recommended_models().await
689689
})
@@ -1445,7 +1445,6 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> {
14451445
let model_config = ModelConfig::new(&model)?;
14461446

14471447
let agent = Agent::new();
1448-
let new_provider = create(&provider_name, model_config).await?;
14491448

14501449
let session = agent
14511450
.config
@@ -1457,8 +1456,8 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> {
14571456
)
14581457
.await?;
14591458

1460-
agent.update_provider(new_provider, &session.id).await?;
1461-
if let Some(config) = get_extension_by_name(&selected_extension_name) {
1459+
let extension_config = get_extension_by_name(&selected_extension_name);
1460+
if let Some(config) = extension_config.as_ref() {
14621461
agent
14631462
.add_extension(config.clone(), &session.id)
14641463
.await
@@ -1478,6 +1477,10 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> {
14781477
return Ok(());
14791478
}
14801479

1480+
let extensions = extension_config.into_iter().collect::<Vec<_>>();
1481+
let new_provider = create(&provider_name, model_config, extensions).await?;
1482+
agent.update_provider(new_provider, &session.id).await?;
1483+
14811484
let permission_manager = PermissionManager::instance();
14821485
let selected_tools = agent
14831486
.list_tools(&session.id, Some(selected_extension_name.clone()))
@@ -1667,7 +1670,7 @@ pub async fn handle_openrouter_auth() -> anyhow::Result<()> {
16671670
}
16681671
};
16691672

1670-
match create("openrouter", model_config).await {
1673+
match create("openrouter", model_config, Vec::new()).await {
16711674
Ok(provider) => {
16721675
let model_config = provider.get_model_config();
16731676
let test_result = provider
@@ -1747,7 +1750,7 @@ pub async fn handle_tetrate_auth() -> anyhow::Result<()> {
17471750
}
17481751
};
17491752

1750-
match create("tetrate", model_config).await {
1753+
match create("tetrate", model_config, Vec::new()).await {
17511754
Ok(provider) => {
17521755
let test_result = provider.fetch_supported_models().await;
17531756

crates/goose-cli/src/commands/web.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,16 +181,16 @@ async fn create_agent(provider_name: &str, model: &str) -> Result<Agent> {
181181
)
182182
.await?;
183183

184-
let provider = goose::providers::create(provider_name, model_config).await?;
185-
agent.update_provider(provider, &init_session.id).await?;
186-
187184
let enabled_configs = goose::config::get_enabled_extensions();
188-
for config in enabled_configs {
185+
for config in &enabled_configs {
189186
if let Err(e) = agent.add_extension(config.clone(), &init_session.id).await {
190187
eprintln!("Warning: Failed to load extension {}: {}", config.name(), e);
191188
}
192189
}
193190

191+
let provider = goose::providers::create(provider_name, model_config, enabled_configs).await?;
192+
agent.update_provider(provider, &init_session.id).await?;
193+
194194
Ok(agent)
195195
}
196196

crates/goose-cli/src/scenario_tests/scenario_runner.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,12 @@ where
187187

188188
let original_env = setup_environment(config)?;
189189

190-
let inner_provider = create(&factory_name, ModelConfig::new(config.model_name)?).await?;
190+
let inner_provider = create(
191+
&factory_name,
192+
ModelConfig::new(config.model_name)?,
193+
Vec::new(),
194+
)
195+
.await?;
191196

192197
let test_provider = Arc::new(TestProvider::new_recording(inner_provider, &file_path));
193198
(

crates/goose-cli/src/session/builder.rs

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -490,17 +490,12 @@ async fn handle_resumed_session_workdir(agent: &Agent, session_id: &str, interac
490490
}
491491
}
492492

493-
async fn resolve_and_load_extensions(
494-
agent: Agent,
493+
async fn resolve_extension_configs(
494+
agent: &Agent,
495495
session_config: &SessionBuilderConfig,
496496
recipe: Option<&Recipe>,
497497
session_id: &str,
498-
provider_for_debug: Arc<dyn goose::providers::base::Provider>,
499-
) -> Arc<Agent> {
500-
for warning in goose::config::get_warnings() {
501-
eprintln!("{}", style(format!("Warning: {}", warning)).yellow());
502-
}
503-
498+
) -> Vec<ExtensionConfig> {
504499
let configured_extensions: Vec<ExtensionConfig> = if session_config.resume {
505500
agent
506501
.config
@@ -523,11 +518,27 @@ async fn resolve_and_load_extensions(
523518
&session_config.builtins,
524519
);
525520

526-
let mut extensions_to_load: Vec<(String, ExtensionConfig)> = configured_extensions
527-
.iter()
528-
.map(|cfg| (cfg.name(), cfg.clone()))
521+
let mut all: Vec<ExtensionConfig> = configured_extensions;
522+
all.extend(cli_flag_extensions.into_iter().map(|(_, cfg)| cfg));
523+
all
524+
}
525+
526+
async fn resolve_and_load_extensions(
527+
agent: Agent,
528+
session_config: &SessionBuilderConfig,
529+
recipe: Option<&Recipe>,
530+
session_id: &str,
531+
provider_for_debug: Arc<dyn goose::providers::base::Provider>,
532+
) -> Arc<Agent> {
533+
for warning in goose::config::get_warnings() {
534+
eprintln!("{}", style(format!("Warning: {}", warning)).yellow());
535+
}
536+
537+
let extensions = resolve_extension_configs(&agent, session_config, recipe, session_id).await;
538+
let extensions_to_load: Vec<(String, ExtensionConfig)> = extensions
539+
.into_iter()
540+
.map(|cfg| (cfg.name(), cfg))
529541
.collect();
530-
extensions_to_load.extend(cli_flag_extensions);
531542

532543
load_extensions(
533544
agent,
@@ -607,7 +618,22 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
607618
)
608619
.await;
609620

610-
let new_provider = match create(&resolved.provider_name, resolved.model_config).await {
621+
let session_id = resolve_session_id(&session_config, &session_manager).await;
622+
623+
if session_config.resume {
624+
handle_resumed_session_workdir(&agent, &session_id, session_config.interactive).await;
625+
}
626+
627+
let extensions_for_provider =
628+
resolve_extension_configs(&agent, &session_config, recipe, &session_id).await;
629+
630+
let new_provider = match create(
631+
&resolved.provider_name,
632+
resolved.model_config,
633+
extensions_for_provider,
634+
)
635+
.await
636+
{
611637
Ok(provider) => provider,
612638
Err(e) => {
613639
output::render_error(&format!(
@@ -633,8 +659,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
633659
tracing::info!("🤖 Using model: {}", resolved.model_name);
634660
}
635661

636-
let session_id = resolve_session_id(&session_config, &session_manager).await;
637-
638662
agent
639663
.update_provider(new_provider, &session_id)
640664
.await
@@ -643,10 +667,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
643667
process::exit(1);
644668
});
645669

646-
if session_config.resume {
647-
handle_resumed_session_workdir(&agent, &session_id, session_config.interactive).await;
648-
}
649-
650670
// Extensions are loaded after session creation because we may change directory when resuming
651671
let agent_ptr = resolve_and_load_extensions(
652672
agent,

0 commit comments

Comments
 (0)