mirror of
https://github.com/openai/codex.git
synced 2026-03-05 06:03:20 +00:00
Compare commits
2 Commits
fix/notify
...
dev/mzeng/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02be9c5827 | ||
|
|
f9d902e957 |
@@ -13,11 +13,12 @@ use codex_core::config::find_codex_home;
|
||||
use codex_core::config::load_global_mcp_servers;
|
||||
use codex_core::config::types::McpServerConfig;
|
||||
use codex_core::config::types::McpServerTransportConfig;
|
||||
use codex_core::mcp::McpServerAuthFlow;
|
||||
use codex_core::mcp::auth::compute_auth_statuses;
|
||||
use codex_core::mcp::install_mcp_server;
|
||||
use codex_core::protocol::McpAuthStatus;
|
||||
use codex_rmcp_client::delete_oauth_tokens;
|
||||
use codex_rmcp_client::perform_oauth_login;
|
||||
use codex_rmcp_client::supports_oauth_login;
|
||||
|
||||
/// Subcommands:
|
||||
/// - `list` — list configured servers (with `--json`)
|
||||
@@ -193,13 +194,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
transport_args,
|
||||
} = add_args;
|
||||
|
||||
validate_server_name(&name)?;
|
||||
|
||||
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
||||
let mut servers = load_global_mcp_servers(&codex_home)
|
||||
.await
|
||||
.with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?;
|
||||
|
||||
let transport = match transport_args {
|
||||
AddMcpTransportArgs {
|
||||
stdio: Some(stdio), ..
|
||||
@@ -238,54 +233,33 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
},
|
||||
AddMcpTransportArgs { .. } => bail!("exactly one of --command or --url must be provided"),
|
||||
};
|
||||
|
||||
let new_entry = McpServerConfig {
|
||||
transport: transport.clone(),
|
||||
enabled: true,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
};
|
||||
|
||||
servers.insert(name.clone(), new_entry);
|
||||
|
||||
ConfigEditsBuilder::new(&codex_home)
|
||||
.replace_mcp_servers(&servers)
|
||||
.apply()
|
||||
.await
|
||||
.with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?;
|
||||
let install_result = install_mcp_server(&codex_home, name.clone(), transport).await?;
|
||||
|
||||
println!("Added global MCP server '{name}'.");
|
||||
|
||||
if let McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var: None,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
} = transport
|
||||
{
|
||||
match supports_oauth_login(&url).await {
|
||||
Ok(true) => {
|
||||
println!("Detected OAuth support. Starting OAuth flow…");
|
||||
perform_oauth_login(
|
||||
&name,
|
||||
&url,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
http_headers.clone(),
|
||||
env_http_headers.clone(),
|
||||
&Vec::new(),
|
||||
config.mcp_oauth_callback_port,
|
||||
)
|
||||
.await?;
|
||||
println!("Successfully logged in.");
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(_) => println!(
|
||||
"MCP server may or may not require login. Run `codex mcp login {name}` to login."
|
||||
),
|
||||
match install_result.auth_flow {
|
||||
McpServerAuthFlow::OAuth {
|
||||
url,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
} => {
|
||||
println!("Detected OAuth support. Starting OAuth flow…");
|
||||
perform_oauth_login(
|
||||
&name,
|
||||
&url,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
&Vec::new(),
|
||||
config.mcp_oauth_callback_port,
|
||||
)
|
||||
.await?;
|
||||
println!("Successfully logged in.");
|
||||
}
|
||||
McpServerAuthFlow::Unknown => println!(
|
||||
"MCP server may or may not require login. Run `codex mcp login {name}` to login."
|
||||
),
|
||||
McpServerAuthFlow::NotRequired => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1106,7 +1106,7 @@ impl Session {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_config(&self) -> std::sync::Arc<Config> {
|
||||
pub(crate) async fn get_config(&self) -> std::sync::Arc<Config> {
|
||||
let state = self.state.lock().await;
|
||||
state
|
||||
.session_configuration
|
||||
@@ -1114,6 +1114,11 @@ impl Session {
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn replace_config(&self, config: Config) {
|
||||
let mut state = self.state.lock().await;
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
}
|
||||
|
||||
pub(crate) async fn new_default_turn_with_sub_id(&self, sub_id: String) -> Arc<TurnContext> {
|
||||
let session_configuration = {
|
||||
let state = self.state.lock().await;
|
||||
|
||||
102
codex-rs/core/src/mcp/install.rs
Normal file
102
codex-rs/core/src/mcp/install.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use codex_rmcp_client::supports_oauth_login;
|
||||
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::config::load_global_mcp_servers;
|
||||
use crate::config::types::McpServerConfig;
|
||||
use crate::config::types::McpServerTransportConfig;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum McpServerAuthFlow {
|
||||
NotRequired,
|
||||
OAuth {
|
||||
url: String,
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct McpServerInstallResult {
|
||||
pub name: String,
|
||||
pub replaced: bool,
|
||||
pub auth_flow: McpServerAuthFlow,
|
||||
pub servers: BTreeMap<String, McpServerConfig>,
|
||||
}
|
||||
|
||||
pub async fn install_mcp_server(
|
||||
codex_home: &Path,
|
||||
name: String,
|
||||
transport: McpServerTransportConfig,
|
||||
) -> Result<McpServerInstallResult> {
|
||||
validate_mcp_server_name(&name)?;
|
||||
|
||||
let mut servers = load_global_mcp_servers(codex_home)
|
||||
.await
|
||||
.with_context(|| format!("failed to load MCP servers from {}", codex_home.display()))?;
|
||||
|
||||
let replaced = servers.contains_key(&name);
|
||||
|
||||
let new_entry = McpServerConfig {
|
||||
transport: transport.clone(),
|
||||
enabled: true,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
};
|
||||
|
||||
servers.insert(name.clone(), new_entry);
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.replace_mcp_servers(&servers)
|
||||
.apply()
|
||||
.await
|
||||
.with_context(|| format!("failed to write MCP servers to {}", codex_home.display()))?;
|
||||
|
||||
let auth_flow = match transport {
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
} if bearer_token_env_var.is_none() => match supports_oauth_login(&url).await {
|
||||
Ok(true) => McpServerAuthFlow::OAuth {
|
||||
url,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
},
|
||||
Ok(false) => McpServerAuthFlow::NotRequired,
|
||||
Err(_) => McpServerAuthFlow::Unknown,
|
||||
},
|
||||
_ => McpServerAuthFlow::NotRequired,
|
||||
};
|
||||
|
||||
Ok(McpServerInstallResult {
|
||||
name,
|
||||
replaced,
|
||||
auth_flow,
|
||||
servers,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_mcp_server_name(name: &str) -> Result<()> {
|
||||
let is_valid = !name.is_empty()
|
||||
&& name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
|
||||
|
||||
if is_valid {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("invalid server name '{name}' (use letters, numbers, '-', '_')")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod auth;
|
||||
mod install;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
@@ -117,6 +118,10 @@ pub(crate) fn with_codex_apps_mcp(
|
||||
servers
|
||||
}
|
||||
|
||||
pub use install::McpServerAuthFlow;
|
||||
pub use install::McpServerInstallResult;
|
||||
pub use install::install_mcp_server;
|
||||
|
||||
pub(crate) fn effective_mcp_servers(
|
||||
config: &Config,
|
||||
auth: Option<&CodexAuth>,
|
||||
|
||||
@@ -157,6 +157,16 @@ pub(crate) struct ToolInfo {
|
||||
pub(crate) connector_name: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) struct AddServerParams {
|
||||
pub(crate) server_name: String,
|
||||
pub(crate) config: McpServerConfig,
|
||||
pub(crate) store_mode: OAuthCredentialsStoreMode,
|
||||
pub(crate) auth_entry: Option<McpAuthStatusEntry>,
|
||||
pub(crate) tx_event: Sender<Event>,
|
||||
pub(crate) cancel_token: CancellationToken,
|
||||
pub(crate) sandbox_state: SandboxState,
|
||||
}
|
||||
|
||||
type ResponderMap = HashMap<(String, RequestId), oneshot::Sender<ElicitationResponse>>;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -311,6 +321,25 @@ pub(crate) struct McpConnectionManager {
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
}
|
||||
|
||||
pub(crate) struct McpServerStartupHandle {
|
||||
server_name: String,
|
||||
client: AsyncManagedClient,
|
||||
auth_entry: Option<McpAuthStatusEntry>,
|
||||
}
|
||||
|
||||
impl McpServerStartupHandle {
|
||||
pub async fn wait(self) -> Result<(), String> {
|
||||
match self.client.client().await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(mcp_init_error_display(
|
||||
self.server_name.as_str(),
|
||||
self.auth_entry.as_ref(),
|
||||
&err,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl McpConnectionManager {
|
||||
pub async fn initialize(
|
||||
&mut self,
|
||||
@@ -416,6 +445,113 @@ impl McpConnectionManager {
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn add_server(
|
||||
&mut self,
|
||||
params: AddServerParams,
|
||||
) -> Result<McpServerStartupHandle, String> {
|
||||
let AddServerParams {
|
||||
server_name,
|
||||
config,
|
||||
store_mode,
|
||||
auth_entry,
|
||||
tx_event,
|
||||
cancel_token,
|
||||
sandbox_state,
|
||||
} = params;
|
||||
if cancel_token.is_cancelled() {
|
||||
return Err("MCP startup was cancelled".to_string());
|
||||
}
|
||||
|
||||
if !config.enabled {
|
||||
return Err(format!("MCP server '{server_name}' is disabled"));
|
||||
}
|
||||
|
||||
let cancel_token = cancel_token.child_token();
|
||||
let _ = emit_update(
|
||||
&tx_event,
|
||||
McpStartupUpdateEvent {
|
||||
server: server_name.clone(),
|
||||
status: McpStartupStatus::Starting,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let async_managed_client = AsyncManagedClient::new(
|
||||
server_name.clone(),
|
||||
config,
|
||||
store_mode,
|
||||
cancel_token,
|
||||
tx_event.clone(),
|
||||
self.elicitation_requests.clone(),
|
||||
);
|
||||
self.clients
|
||||
.insert(server_name.clone(), async_managed_client.clone());
|
||||
|
||||
let async_managed_client_for_task = async_managed_client.clone();
|
||||
let auth_entry_for_task = auth_entry.clone();
|
||||
let server_name_for_task = server_name.clone();
|
||||
let sandbox_state_for_task = sandbox_state.clone();
|
||||
tokio::spawn(async move {
|
||||
let outcome = async_managed_client_for_task.client().await;
|
||||
let status = match &outcome {
|
||||
Ok(_) => {
|
||||
if let Err(err) = async_managed_client_for_task
|
||||
.notify_sandbox_state_change(&sandbox_state_for_task)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Failed to notify sandbox state to MCP server {server_name_for_task}: {err:#}",
|
||||
);
|
||||
}
|
||||
McpStartupStatus::Ready
|
||||
}
|
||||
Err(StartupOutcomeError::Cancelled) => McpStartupStatus::Cancelled,
|
||||
Err(error) => {
|
||||
let error_str = mcp_init_error_display(
|
||||
server_name_for_task.as_str(),
|
||||
auth_entry_for_task.as_ref(),
|
||||
error,
|
||||
);
|
||||
McpStartupStatus::Failed { error: error_str }
|
||||
}
|
||||
};
|
||||
|
||||
let _ = emit_update(
|
||||
&tx_event,
|
||||
McpStartupUpdateEvent {
|
||||
server: server_name_for_task.clone(),
|
||||
status,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut summary = McpStartupCompleteEvent::default();
|
||||
match outcome {
|
||||
Ok(_) => summary.ready.push(server_name_for_task),
|
||||
Err(StartupOutcomeError::Cancelled) => summary.cancelled.push(server_name_for_task),
|
||||
Err(StartupOutcomeError::Failed { error }) => {
|
||||
summary.failed.push(McpStartupFailure {
|
||||
server: server_name_for_task,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tx_event
|
||||
.send(Event {
|
||||
id: INITIAL_SUBMIT_ID.to_owned(),
|
||||
msg: EventMsg::McpStartupComplete(summary),
|
||||
})
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(McpServerStartupHandle {
|
||||
server_name,
|
||||
client: async_managed_client,
|
||||
auth_entry,
|
||||
})
|
||||
}
|
||||
|
||||
async fn client_by_name(&self, name: &str) -> Result<ManagedClient> {
|
||||
self.clients
|
||||
.get(name)
|
||||
|
||||
@@ -516,7 +516,7 @@ mod tests {
|
||||
)
|
||||
.unwrap_or_else(|_| cfg.codex_home.join("skills/pdf-processing/SKILL.md"));
|
||||
let expected_path_str = expected_path.to_string_lossy().replace('\\', "/");
|
||||
let usage_rules = "- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.\n- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.\n- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.\n- How to use a skill (progressive disclosure):\n 1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.\n 2) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.\n 3) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.\n 4) If `assets/` or templates exist, reuse them instead of recreating from scratch.\n- Coordination and sequencing:\n - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.\n - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.\n- Context hygiene:\n - Keep context small: summarize long sections instead of pasting them; only load extra files when needed.\n - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.\n - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.\n- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.";
|
||||
let usage_rules = "- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.\n- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.\n- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.\n- MCP availability: If MCP tools are required in this skill but not available, use the install_mcp_tool tool to install the missing MCPs.\n- How to use a skill (progressive disclosure):\n 1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.\n 2) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.\n 3) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.\n 4) If `assets/` or templates exist, reuse them instead of recreating from scratch.\n- Coordination and sequencing:\n - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.\n - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.\n- Context hygiene:\n - Keep context small: summarize long sections instead of pasting them; only load extra files when needed.\n - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.\n - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.\n- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.";
|
||||
let expected = format!(
|
||||
"base doc\n\n## Skills\nA skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.\n### Available skills\n- pdf-processing: extract from pdfs (file: {expected_path_str})\n### How to use skills\n{usage_rules}"
|
||||
);
|
||||
@@ -540,7 +540,7 @@ mod tests {
|
||||
dunce::canonicalize(cfg.codex_home.join("skills/linting/SKILL.md").as_path())
|
||||
.unwrap_or_else(|_| cfg.codex_home.join("skills/linting/SKILL.md"));
|
||||
let expected_path_str = expected_path.to_string_lossy().replace('\\', "/");
|
||||
let usage_rules = "- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.\n- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.\n- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.\n- How to use a skill (progressive disclosure):\n 1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.\n 2) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.\n 3) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.\n 4) If `assets/` or templates exist, reuse them instead of recreating from scratch.\n- Coordination and sequencing:\n - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.\n - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.\n- Context hygiene:\n - Keep context small: summarize long sections instead of pasting them; only load extra files when needed.\n - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.\n - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.\n- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.";
|
||||
let usage_rules = "- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.\n- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.\n- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.\n- MCP availability: If MCP tools are required in this skill but not available, use the install_mcp_tool tool to install the missing MCPs.\n- How to use a skill (progressive disclosure):\n 1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.\n 2) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.\n 3) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.\n 4) If `assets/` or templates exist, reuse them instead of recreating from scratch.\n- Coordination and sequencing:\n - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.\n - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.\n- Context hygiene:\n - Keep context small: summarize long sections instead of pasting them; only load extra files when needed.\n - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.\n - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.\n- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.";
|
||||
let expected = format!(
|
||||
"## Skills\nA skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.\n### Available skills\n- linting: run clippy (file: {expected_path_str})\n### How to use skills\n{usage_rules}"
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ pub fn render_skills_section(skills: &[SkillMetadata]) -> Option<String> {
|
||||
r###"- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
|
||||
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
|
||||
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
|
||||
- MCP availability: If MCP tools are required in this skill but not available, use the install_mcp_tool tool to install the missing MCPs.
|
||||
- How to use a skill (progressive disclosure):
|
||||
1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.
|
||||
2) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
|
||||
|
||||
462
codex-rs/core/src/tools/handlers/mcp_install.rs
Normal file
462
codex-rs/core/src/tools/handlers/mcp_install.rs
Normal file
@@ -0,0 +1,462 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestion;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
|
||||
use codex_rmcp_client::perform_oauth_login;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::config::types::McpServerTransportConfig;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::mcp::McpServerAuthFlow;
|
||||
use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::mcp::install_mcp_server;
|
||||
use crate::mcp_connection_manager::AddServerParams;
|
||||
use crate::mcp_connection_manager::DEFAULT_STARTUP_TIMEOUT;
|
||||
use crate::mcp_connection_manager::SandboxState;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
|
||||
use super::parse_arguments;
|
||||
|
||||
const DEFAULT_OAUTH_TIMEOUT_SECS: i64 = 300;
|
||||
const RELOAD_TIMEOUT_BUFFER_SECS: u64 = 5;
|
||||
const MCP_INSTALL_OPTION_INSTALL: &str = "Install tool";
|
||||
const MCP_INSTALL_OPTION_RUN_ANYWAY: &str = "Continue anyway";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct McpInstallApprovalKey {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct InstallMcpToolArgs {
|
||||
name: String,
|
||||
description: String,
|
||||
transport: String,
|
||||
command: Option<Vec<String>>,
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InstallMcpToolResponse {
|
||||
name: String,
|
||||
description: String,
|
||||
installed: bool,
|
||||
replaced: bool,
|
||||
reloaded: bool,
|
||||
oauth_status: OauthStatus,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
notes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum OauthStatus {
|
||||
NotRequired,
|
||||
Completed,
|
||||
Skipped,
|
||||
}
|
||||
|
||||
pub struct McpInstallHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl ToolHandler for McpInstallHandler {
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
call_id,
|
||||
payload,
|
||||
..
|
||||
} = invocation;
|
||||
|
||||
let arguments = match payload {
|
||||
ToolPayload::Function { arguments } => arguments,
|
||||
_ => {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"install_mcp_tool handler received unsupported payload".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let args: InstallMcpToolArgs = parse_arguments(&arguments)?;
|
||||
let name = normalize_required_string("name", args.name)?;
|
||||
let description = normalize_required_string("description", args.description)?;
|
||||
let transport = normalize_required_string("transport", args.transport)?;
|
||||
let transport_kind = transport.to_ascii_lowercase().replace('-', "_");
|
||||
|
||||
let transport_config = match transport_kind.as_str() {
|
||||
"stdio" => {
|
||||
let mut command_parts = args.command.unwrap_or_default().into_iter();
|
||||
let command_bin = command_parts.next().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"command is required when transport is stdio".to_string(),
|
||||
)
|
||||
})?;
|
||||
let command_args = command_parts.collect();
|
||||
McpServerTransportConfig::Stdio {
|
||||
command: command_bin,
|
||||
args: command_args,
|
||||
env: None,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
}
|
||||
}
|
||||
"streamable_http" => {
|
||||
let url = normalize_required_option("url", args.url)?;
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
}
|
||||
}
|
||||
other => {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"unsupported transport '{other}'; expected 'stdio' or 'streamable_http'",
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
ensure_mcp_install_approval(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
&call_id,
|
||||
&name,
|
||||
&description,
|
||||
)
|
||||
.await?;
|
||||
|
||||
session
|
||||
.notify_background_event(
|
||||
&turn,
|
||||
format!("Installing MCP tool '{name}': {description}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let config = session.get_config().await;
|
||||
let install_result = install_mcp_server(&config.codex_home, name.clone(), transport_config)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"failed to install MCP tool '{name}': {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
if install_result.replaced {
|
||||
session
|
||||
.notify_background_event(&turn, format!("Updated existing MCP tool '{name}'."))
|
||||
.await;
|
||||
} else {
|
||||
session
|
||||
.notify_background_event(&turn, format!("Added MCP tool '{name}'."))
|
||||
.await;
|
||||
}
|
||||
|
||||
let mut notes = Vec::new();
|
||||
let mut oauth_status = OauthStatus::NotRequired;
|
||||
|
||||
match install_result.auth_flow {
|
||||
McpServerAuthFlow::OAuth {
|
||||
url,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
} => {
|
||||
let timeout_secs = DEFAULT_OAUTH_TIMEOUT_SECS;
|
||||
if timeout_secs <= 0 {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"oauth_timeout_secs must be greater than zero".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
session
|
||||
.notify_background_event(
|
||||
&turn,
|
||||
format!("Detected OAuth support. Starting OAuth flow for '{name}'..."),
|
||||
)
|
||||
.await;
|
||||
|
||||
session
|
||||
.notify_background_event(
|
||||
&turn,
|
||||
format!("Launching browser for OAuth login for '{name}'..."),
|
||||
)
|
||||
.await;
|
||||
|
||||
let login_result = timeout(
|
||||
Duration::from_secs(timeout_secs as u64),
|
||||
perform_oauth_login(
|
||||
&name,
|
||||
&url,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
&[],
|
||||
config.mcp_oauth_callback_port,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
match login_result {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"OAuth login failed for '{name}': {err}"
|
||||
)));
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"timed out while waiting for OAuth login for '{name}'"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
oauth_status = OauthStatus::Completed;
|
||||
session
|
||||
.notify_background_event(&turn, format!("OAuth login completed for '{name}'."))
|
||||
.await;
|
||||
}
|
||||
McpServerAuthFlow::Unknown => {
|
||||
oauth_status = OauthStatus::Skipped;
|
||||
notes.push(format!(
|
||||
"OAuth support could not be detected. If '{name}' requires login, run `codex mcp login {name}`."
|
||||
));
|
||||
}
|
||||
McpServerAuthFlow::NotRequired => {}
|
||||
}
|
||||
|
||||
let mut updated_config = (*config).clone();
|
||||
let servers: HashMap<_, _> = install_result.servers.clone().into_iter().collect();
|
||||
updated_config.mcp_servers.set(servers).map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"failed to update session config for '{name}': {err}"
|
||||
))
|
||||
})?;
|
||||
session.replace_config(updated_config).await;
|
||||
|
||||
let server_config = install_result.servers.get(&name).cloned().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"failed to load MCP server config for '{name}'"
|
||||
))
|
||||
})?;
|
||||
|
||||
let auth_entries = compute_auth_statuses(
|
||||
std::iter::once((&name, &server_config)),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
)
|
||||
.await;
|
||||
let auth_entry = auth_entries.get(&name).cloned();
|
||||
|
||||
session
|
||||
.notify_background_event(&turn, format!("Reloading MCP server '{name}'..."))
|
||||
.await;
|
||||
|
||||
let reload_timeout = server_config
|
||||
.startup_timeout_sec
|
||||
.unwrap_or(DEFAULT_STARTUP_TIMEOUT)
|
||||
+ Duration::from_secs(RELOAD_TIMEOUT_BUFFER_SECS);
|
||||
|
||||
let sandbox_state = SandboxState {
|
||||
sandbox_policy: turn.sandbox_policy.clone(),
|
||||
codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.clone(),
|
||||
sandbox_cwd: turn.cwd.clone(),
|
||||
};
|
||||
let cancel_token = session
|
||||
.services
|
||||
.mcp_startup_cancellation_token
|
||||
.lock()
|
||||
.await
|
||||
.clone();
|
||||
let startup_handle = {
|
||||
let mut manager = session.services.mcp_connection_manager.write().await;
|
||||
manager
|
||||
.add_server(AddServerParams {
|
||||
server_name: name.clone(),
|
||||
config: server_config,
|
||||
store_mode: config.mcp_oauth_credentials_store_mode,
|
||||
auth_entry,
|
||||
tx_event: session.get_tx_event(),
|
||||
cancel_token,
|
||||
sandbox_state,
|
||||
})
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)?
|
||||
};
|
||||
|
||||
match timeout(reload_timeout, startup_handle.wait()).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => {
|
||||
return Err(FunctionCallError::RespondToModel(err));
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"timed out while waiting for MCP server '{name}' to start"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let response = InstallMcpToolResponse {
|
||||
name,
|
||||
description,
|
||||
installed: true,
|
||||
replaced: install_result.replaced,
|
||||
reloaded: true,
|
||||
oauth_status,
|
||||
notes,
|
||||
};
|
||||
|
||||
serialize_function_output(response)
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_mcp_install_approval(
|
||||
session: &Session,
|
||||
turn: &TurnContext,
|
||||
call_id: &str,
|
||||
name: &str,
|
||||
description: &str,
|
||||
) -> Result<(), FunctionCallError> {
|
||||
let approval_key = McpInstallApprovalKey {
|
||||
name: name.to_string(),
|
||||
};
|
||||
let already_approved = {
|
||||
let store = session.services.tool_approvals.lock().await;
|
||||
matches!(
|
||||
store.get(&approval_key),
|
||||
Some(ReviewDecision::ApprovedForSession)
|
||||
)
|
||||
};
|
||||
if already_approved {
|
||||
return Ok(());
|
||||
}
|
||||
if matches!(
|
||||
turn.sandbox_policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let question_id = format!("mcp_install_{name}");
|
||||
let question = RequestUserInputQuestion {
|
||||
id: question_id.clone(),
|
||||
header: format!("Install MCP server '{name}'?"),
|
||||
question: format!(
|
||||
"The agent wants to install the MCP server '{name}' ({description}). This choice will be remembered for the rest of this session."
|
||||
),
|
||||
options: Some(vec![
|
||||
RequestUserInputQuestionOption {
|
||||
label: MCP_INSTALL_OPTION_INSTALL.to_string(),
|
||||
description: "Install and load this MCP server.".to_string(),
|
||||
},
|
||||
RequestUserInputQuestionOption {
|
||||
label: MCP_INSTALL_OPTION_RUN_ANYWAY.to_string(),
|
||||
description: "Proceed without further review.".to_string(),
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
let response = session
|
||||
.request_user_input(
|
||||
turn,
|
||||
call_id.to_string(),
|
||||
RequestUserInputArgs {
|
||||
questions: vec![question],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"install_mcp_tool was cancelled before receiving a response".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let selection = response
|
||||
.answers
|
||||
.get(&question_id)
|
||||
.and_then(|answer| answer.answers.first())
|
||||
.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"install_mcp_tool requires an explicit selection".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !matches!(
|
||||
selection.as_str(),
|
||||
MCP_INSTALL_OPTION_INSTALL | MCP_INSTALL_OPTION_RUN_ANYWAY
|
||||
) {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"install_mcp_tool received unsupported selection '{selection}'"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut store = session.services.tool_approvals.lock().await;
|
||||
store.put(approval_key, ReviewDecision::ApprovedForSession);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_optional_string(input: Option<String>) -> Option<String> {
|
||||
input.and_then(|value| {
|
||||
let trimmed = value.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_required_string(field: &str, value: String) -> Result<String, FunctionCallError> {
|
||||
match normalize_optional_string(Some(value)) {
|
||||
Some(normalized) => Ok(normalized),
|
||||
None => Err(FunctionCallError::RespondToModel(format!(
|
||||
"{field} must be provided"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_required_option(
|
||||
field: &str,
|
||||
value: Option<String>,
|
||||
) -> Result<String, FunctionCallError> {
|
||||
let value = value.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"{field} is required when transport is streamable_http"
|
||||
))
|
||||
})?;
|
||||
normalize_required_string(field, value)
|
||||
}
|
||||
|
||||
fn serialize_function_output<T>(payload: T) -> Result<ToolOutput, FunctionCallError>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let content = serde_json::to_string(&payload).map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"failed to serialize MCP install response: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(ToolOutput::Function {
|
||||
content,
|
||||
content_items: None,
|
||||
success: Some(true),
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pub(crate) mod collab;
|
||||
mod grep_files;
|
||||
mod list_dir;
|
||||
mod mcp;
|
||||
mod mcp_install;
|
||||
mod mcp_resource;
|
||||
mod plan;
|
||||
mod read_file;
|
||||
@@ -21,6 +22,7 @@ pub use collab::CollabHandler;
|
||||
pub use grep_files::GrepFilesHandler;
|
||||
pub use list_dir::ListDirHandler;
|
||||
pub use mcp::McpHandler;
|
||||
pub use mcp_install::McpInstallHandler;
|
||||
pub use mcp_resource::McpResourceHandler;
|
||||
pub use plan::PlanHandler;
|
||||
pub use read_file::ReadFileHandler;
|
||||
|
||||
@@ -1008,6 +1008,70 @@ fn create_read_mcp_resource_tool() -> ToolSpec {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn create_install_mcp_tool() -> ToolSpec {
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"name".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Name for the MCP server to install (letters, numbers, '-' or '_')."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"description".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Short description of the MCP tool/server being installed.".to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"transport".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Transport type: 'stdio' or 'streamable_http'.".to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"command".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(JsonSchema::String { description: None }),
|
||||
description: Some(
|
||||
"Command and args for stdio transport; first entry is the executable."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"url".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"URL for streamable HTTP MCP servers; required when transport is streamable_http."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "install_mcp_tool".to_string(),
|
||||
description: "Use when the model needs to use an mcp tool but it's not available. You must call this tool instead whenever you want to run the `codex mcp add` command. Before calling this tool, model must search online (if allowed) for the exact mcp config params required to install this mcp, until you are confident it has the correct configuration or it cannot figure it out.".to_string(),
|
||||
strict: false,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec![
|
||||
"name".to_string(),
|
||||
"description".to_string(),
|
||||
"transport".to_string(),
|
||||
]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
})
|
||||
}
|
||||
/// TODO(dylan): deprecate once we get rid of json tool
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct ApplyPatchToolArgs {
|
||||
@@ -1222,6 +1286,7 @@ pub(crate) fn build_specs(
|
||||
use crate::tools::handlers::GrepFilesHandler;
|
||||
use crate::tools::handlers::ListDirHandler;
|
||||
use crate::tools::handlers::McpHandler;
|
||||
use crate::tools::handlers::McpInstallHandler;
|
||||
use crate::tools::handlers::McpResourceHandler;
|
||||
use crate::tools::handlers::PlanHandler;
|
||||
use crate::tools::handlers::ReadFileHandler;
|
||||
@@ -1242,6 +1307,7 @@ pub(crate) fn build_specs(
|
||||
let view_image_handler = Arc::new(ViewImageHandler);
|
||||
let mcp_handler = Arc::new(McpHandler);
|
||||
let mcp_resource_handler = Arc::new(McpResourceHandler);
|
||||
let mcp_install_handler = Arc::new(McpInstallHandler);
|
||||
let shell_command_handler = Arc::new(ShellCommandHandler);
|
||||
let request_user_input_handler = Arc::new(RequestUserInputHandler);
|
||||
|
||||
@@ -1277,9 +1343,11 @@ pub(crate) fn build_specs(
|
||||
builder.push_spec_with_parallel_support(create_list_mcp_resources_tool(), true);
|
||||
builder.push_spec_with_parallel_support(create_list_mcp_resource_templates_tool(), true);
|
||||
builder.push_spec_with_parallel_support(create_read_mcp_resource_tool(), true);
|
||||
builder.push_spec(create_install_mcp_tool());
|
||||
builder.register_handler("list_mcp_resources", mcp_resource_handler.clone());
|
||||
builder.register_handler("list_mcp_resource_templates", mcp_resource_handler.clone());
|
||||
builder.register_handler("read_mcp_resource", mcp_resource_handler);
|
||||
builder.register_handler("install_mcp_tool", mcp_install_handler);
|
||||
|
||||
builder.push_spec(PLAN_TOOL.clone());
|
||||
builder.register_handler("update_plan", plan_handler);
|
||||
@@ -1522,6 +1590,7 @@ mod tests {
|
||||
create_list_mcp_resources_tool(),
|
||||
create_list_mcp_resource_templates_tool(),
|
||||
create_read_mcp_resource_tool(),
|
||||
create_install_mcp_tool(),
|
||||
PLAN_TOOL.clone(),
|
||||
create_request_user_input_tool(),
|
||||
create_apply_patch_freeform_tool(),
|
||||
@@ -1669,6 +1738,7 @@ mod tests {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
@@ -1691,6 +1761,7 @@ mod tests {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
@@ -1715,6 +1786,7 @@ mod tests {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
@@ -1739,6 +1811,7 @@ mod tests {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
@@ -1761,6 +1834,7 @@ mod tests {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"web_search",
|
||||
@@ -1782,6 +1856,7 @@ mod tests {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
@@ -1804,6 +1879,7 @@ mod tests {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"web_search",
|
||||
@@ -1825,6 +1901,7 @@ mod tests {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
@@ -1848,6 +1925,7 @@ mod tests {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
@@ -1872,6 +1950,7 @@ mod tests {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"web_search",
|
||||
|
||||
@@ -65,6 +65,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resources".to_string(),
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"install_mcp_tool".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"web_search".to_string(),
|
||||
@@ -81,6 +82,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resources".to_string(),
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"install_mcp_tool".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"apply_patch".to_string(),
|
||||
@@ -98,6 +100,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resources".to_string(),
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"install_mcp_tool".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"apply_patch".to_string(),
|
||||
@@ -115,6 +118,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resources".to_string(),
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"install_mcp_tool".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"web_search".to_string(),
|
||||
@@ -131,6 +135,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resources".to_string(),
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"install_mcp_tool".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"apply_patch".to_string(),
|
||||
@@ -148,6 +153,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resources".to_string(),
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"install_mcp_tool".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"apply_patch".to_string(),
|
||||
|
||||
@@ -136,6 +136,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
|
||||
"list_mcp_resources",
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"install_mcp_tool",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
|
||||
Reference in New Issue
Block a user