Compare commits

...

1 Commits

Author SHA1 Message Date
Gabriel Peal
9f9ba240ec Use env 2025-10-07 10:53:37 -07:00
7 changed files with 464 additions and 71 deletions

2
codex-rs/Cargo.lock generated
View File

@@ -992,7 +992,7 @@ dependencies = [
"tokio-stream",
"tracing",
"tracing-subscriber",
"unicode-width 0.1.14",
"unicode-width 0.2.1",
]
[[package]]

View File

@@ -1,4 +1,8 @@
use std::collections::HashMap;
use std::fs;
use std::io::IsTerminal;
use std::io::Read;
use std::path::Path;
use anyhow::Context;
use anyhow::Result;
@@ -81,8 +85,28 @@ pub struct AddArgs {
#[arg(long, value_parser = parse_env_pair, value_name = "KEY=VALUE")]
pub env: Vec<(String, String)>,
/// URL for a streamable HTTP MCP server.
#[arg(long, value_name = "URL")]
pub url: Option<String>,
/// Optional environment variable to read for a bearer token.
#[arg(
long = "bearer-token-env-var",
value_name = "ENV_VAR",
requires = "url"
)]
pub bearer_token_env_var: Option<String>,
/// Read a bearer token for the HTTP server from stdin and persist it to ~/.codex/.env.
#[arg(
long = "with-bearer-token",
requires = "url",
help = "Read the MCP bearer token from stdin (e.g. `printenv GITHUB_API_KEY | codex mcp add github --url <URL> --with-bearer-token`)"
)]
pub with_bearer_token: bool,
/// Command to launch the MCP server.
#[arg(trailing_var_arg = true, num_args = 1..)]
#[arg(trailing_var_arg = true, num_args = 0..)]
pub command: Vec<String>,
}
@@ -136,43 +160,87 @@ impl McpCli {
}
}
const DEFAULT_BEARER_TOKEN_ENV_PREFIX: &str = "CODEX_MCP";
async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<()> {
// Validate any provided overrides even though they are not currently applied.
config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
let AddArgs { name, env, command } = add_args;
let AddArgs {
name,
env,
url,
bearer_token_env_var,
with_bearer_token,
command,
} = add_args;
validate_server_name(&name)?;
let mut command_parts = command.into_iter();
let command_bin = command_parts
.next()
.ok_or_else(|| anyhow!("command is required"))?;
let command_args: Vec<String> = command_parts.collect();
let env_map = if env.is_empty() {
None
} else {
let mut map = HashMap::new();
for (key, value) in env {
map.insert(key, value);
}
Some(map)
};
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 new_entry = McpServerConfig {
transport: McpServerTransportConfig::Stdio {
command: command_bin,
args: command_args,
env: env_map,
},
startup_timeout_sec: None,
tool_timeout_sec: None,
let new_entry = if let Some(url) = url {
if !env.is_empty() {
bail!("--env is not supported when adding a streamable HTTP server");
}
if !command.is_empty() {
bail!("command arguments are not supported when --url is provided");
}
let mut env_var_name = bearer_token_env_var;
if with_bearer_token {
let token = read_bearer_token_from_stdin()?;
let key = env_var_name
.take()
.unwrap_or_else(|| derive_bearer_token_env_var(&name));
persist_env_var(&codex_home, &key, &token)?;
env_var_name = Some(key);
}
McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var: env_var_name,
},
startup_timeout_sec: None,
tool_timeout_sec: None,
}
} else {
if with_bearer_token {
bail!("--with-bearer-token can only be used with --url");
}
if bearer_token_env_var.is_some() {
bail!("--bearer-token-env-var can only be used with --url");
}
let mut command_parts = command.into_iter();
let command_bin = command_parts
.next()
.ok_or_else(|| anyhow!("command is required"))?;
let command_args: Vec<String> = command_parts.collect();
let env_map = if env.is_empty() {
None
} else {
let mut map = HashMap::new();
for (key, value) in env {
map.insert(key, value);
}
Some(map)
};
McpServerConfig {
transport: McpServerTransportConfig::Stdio {
command: command_bin,
args: command_args,
env: env_map,
},
startup_timeout_sec: None,
tool_timeout_sec: None,
}
};
servers.insert(name.clone(), new_entry);
@@ -288,11 +356,14 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
"args": args,
"env": env,
}),
McpServerTransportConfig::StreamableHttp { url, bearer_token } => {
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => {
serde_json::json!({
"type": "streamable_http",
"url": url,
"bearer_token": bearer_token,
"bearer_token_env_var": bearer_token_env_var,
})
}
};
@@ -345,13 +416,15 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
};
stdio_rows.push([name.clone(), command.clone(), args_display, env_display]);
}
McpServerTransportConfig::StreamableHttp { url, bearer_token } => {
let has_bearer = if bearer_token.is_some() {
"True"
} else {
"False"
};
http_rows.push([name.clone(), url.clone(), has_bearer.into()]);
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => {
http_rows.push([
name.clone(),
url.clone(),
bearer_token_env_var.clone().unwrap_or("-".to_string()),
]);
}
}
}
@@ -396,7 +469,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
}
if !http_rows.is_empty() {
let mut widths = ["Name".len(), "Url".len(), "Has Bearer Token".len()];
let mut widths = ["Name".len(), "Url".len(), "Bearer Token Env Var".len()];
for row in &http_rows {
for (i, cell) in row.iter().enumerate() {
widths[i] = widths[i].max(cell.len());
@@ -407,7 +480,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
"{:<name_w$} {:<url_w$} {:<token_w$}",
"Name",
"Url",
"Has Bearer Token",
"Bearer Token Env Var",
name_w = widths[0],
url_w = widths[1],
token_w = widths[2],
@@ -447,10 +520,13 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
"args": args,
"env": env,
}),
McpServerTransportConfig::StreamableHttp { url, bearer_token } => serde_json::json!({
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => serde_json::json!({
"type": "streamable_http",
"url": url,
"bearer_token": bearer_token,
"bearer_token_env_var": bearer_token_env_var,
}),
};
let output = serde_json::to_string_pretty(&serde_json::json!({
@@ -493,11 +569,14 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
};
println!(" env: {env_display}");
}
McpServerTransportConfig::StreamableHttp { url, bearer_token } => {
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => {
println!(" transport: streamable_http");
println!(" url: {url}");
let bearer = bearer_token.as_deref().unwrap_or("-");
println!(" bearer_token: {bearer}");
let env_var = bearer_token_env_var.as_deref().unwrap_or("-");
println!(" bearer_token_env_var: {env_var}");
}
}
if let Some(timeout) = server.startup_timeout_sec {
@@ -538,3 +617,87 @@ fn validate_server_name(name: &str) -> Result<()> {
bail!("invalid server name '{name}' (use letters, numbers, '-', '_')");
}
}
fn read_bearer_token_from_stdin() -> Result<String> {
let mut stdin = std::io::stdin();
if stdin.is_terminal() {
bail!(
"--with-bearer-token expects the bearer token on stdin. Try piping it, e.g. `printenv GITHUB_API_KEY | codex mcp add <name> --url <url> --with-bearer-token`."
);
}
eprintln!("Reading MCP bearer token from stdin...");
let mut buffer = String::new();
stdin
.read_to_string(&mut buffer)
.context("failed to read bearer token from stdin")?;
let token = buffer.trim().to_string();
if token.is_empty() {
bail!("No bearer token provided via stdin.");
}
Ok(token)
}
fn derive_bearer_token_env_var(server_name: &str) -> String {
let mut normalized = String::new();
for ch in server_name.chars() {
if ch.is_ascii_alphanumeric() {
normalized.push(ch.to_ascii_uppercase());
} else {
normalized.push('_');
}
}
format!("{DEFAULT_BEARER_TOKEN_ENV_PREFIX}_{normalized}_BEARER_TOKEN")
}
fn persist_env_var(codex_home: &Path, key: &str, value: &str) -> Result<()> {
let dotenv_path = codex_home.join(".env");
if let Some(parent) = dotenv_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory {}", parent.display()))?;
}
let mut lines = Vec::new();
let mut replaced = false;
if let Ok(existing) = fs::read_to_string(&dotenv_path) {
for raw_line in existing.lines() {
if let Some((existing_key, had_export)) = parse_env_key(raw_line)
&& existing_key == key
{
let prefix = if had_export { "export " } else { "" };
lines.push(format!("{prefix}{key}={value}"));
replaced = true;
continue;
}
lines.push(raw_line.to_string());
}
}
if !replaced {
lines.push(format!("{key}={value}"));
}
let mut contents = lines.join("\n");
if !contents.ends_with('\n') {
contents.push('\n');
}
fs::write(&dotenv_path, contents)
.with_context(|| format!("failed to update {}", dotenv_path.display()))?;
Ok(())
}
fn parse_env_key(line: &str) -> Option<(String, bool)> {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
return None;
}
let (trimmed, had_export) = if let Some(stripped) = trimmed.strip_prefix("export ") {
(stripped, true)
} else {
(trimmed, false)
};
let (key, _) = trimmed.split_once('=')?;
Some((key.trim().to_string(), had_export))
}

View File

@@ -1,3 +1,4 @@
use std::fs;
use std::path::Path;
use anyhow::Result;
@@ -93,3 +94,107 @@ async fn add_with_env_preserves_key_order_and_values() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn add_streamable_http_with_bearer_token_env_var() -> Result<()> {
let codex_home = TempDir::new()?;
let dotenv_path = codex_home.path().join(".env");
let mut add_cmd = codex_command(codex_home.path())?;
add_cmd
.write_stdin("super-secret-token")
.args([
"mcp",
"add",
"github",
"--url",
"https://example.com/mcp",
"--with-bearer-token",
])
.assert()
.success();
let servers = load_global_mcp_servers(codex_home.path()).await?;
let github = servers.get("github").expect("github server should exist");
match &github.transport {
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => {
assert_eq!(url, "https://example.com/mcp");
assert_eq!(
bearer_token_env_var.as_deref(),
Some("CODEX_MCP_GITHUB_BEARER_TOKEN")
);
}
other => panic!("unexpected transport: {other:?}"),
}
let dotenv_contents = fs::read_to_string(&dotenv_path)?;
assert!(dotenv_contents.contains("CODEX_MCP_GITHUB_BEARER_TOKEN=super-secret-token"));
Ok(())
}
#[tokio::test]
async fn add_streamable_http_with_custom_env_var() -> Result<()> {
let codex_home = TempDir::new()?;
let mut add_cmd = codex_command(codex_home.path())?;
add_cmd
.write_stdin("another-secret")
.args([
"mcp",
"add",
"issues",
"--url",
"https://example.com/issues",
"--bearer-token-env-var",
"GITHUB_TOKEN",
"--with-bearer-token",
])
.assert()
.success();
let servers = load_global_mcp_servers(codex_home.path()).await?;
let issues = servers.get("issues").expect("issues server should exist");
match &issues.transport {
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => {
assert_eq!(url, "https://example.com/issues");
assert_eq!(bearer_token_env_var.as_deref(), Some("GITHUB_TOKEN"));
}
other => panic!("unexpected transport: {other:?}"),
}
let dotenv_contents = fs::read_to_string(codex_home.path().join(".env"))?;
assert!(dotenv_contents.contains("GITHUB_TOKEN=another-secret"));
Ok(())
}
#[tokio::test]
async fn add_streamable_http_with_bearer_token_requires_stdin() -> Result<()> {
let codex_home = TempDir::new()?;
let mut add_cmd = codex_command(codex_home.path())?;
add_cmd
.args([
"mcp",
"add",
"github",
"--url",
"https://example.com/mcp",
"--with-bearer-token",
])
.assert()
.failure()
.stderr(contains("No bearer token provided via stdin."));
let servers = load_global_mcp_servers(codex_home.path()).await?;
assert!(servers.is_empty());
Ok(())
}

View File

@@ -37,8 +37,10 @@ use dirs::home_dir;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use tempfile::NamedTempFile;
use toml::Value as TomlValue;
use toml_edit::Array as TomlArray;
@@ -301,12 +303,35 @@ pub async fn load_global_mcp_servers(
return Ok(BTreeMap::new());
};
ensure_no_inline_bearer_tokens(servers_value)?;
servers_value
.clone()
.try_into()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
/// We briefly allowed plain text bearer_token fields in MCP server configs.
/// We want to warn people who recently added these fields but can remove this after a few months.
fn ensure_no_inline_bearer_tokens(value: &TomlValue) -> std::io::Result<()> {
let Some(servers_table) = value.as_table() else {
return Ok(());
};
for (server_name, server_value) in servers_table {
if let Some(server_table) = server_value.as_table()
&& server_table.contains_key("bearer_token")
{
let message = format!(
"mcp_servers.{server_name} uses unsupported `bearer_token`; set `bearer_token_env_var` or rerun `codex mcp add {server_name} --with-bearer-token`."
);
return Err(std::io::Error::new(ErrorKind::InvalidData, message));
}
}
Ok(())
}
pub fn write_global_mcp_servers(
codex_home: &Path,
servers: &BTreeMap<String, McpServerConfig>,
@@ -355,10 +380,13 @@ pub fn write_global_mcp_servers(
entry["env"] = TomlItem::Table(env_table);
}
}
McpServerTransportConfig::StreamableHttp { url, bearer_token } => {
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => {
entry["url"] = toml_edit::value(url.clone());
if let Some(token) = bearer_token {
entry["bearer_token"] = toml_edit::value(token.clone());
if let Some(env_var) = bearer_token_env_var {
entry["bearer_token_env_var"] = toml_edit::value(env_var.clone());
}
}
}
@@ -1471,6 +1499,31 @@ startup_timeout_ms = 2500
Ok(())
}
#[tokio::test]
async fn load_global_mcp_servers_rejects_inline_bearer_token() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let config_path = codex_home.path().join(CONFIG_TOML_FILE);
std::fs::write(
&config_path,
r#"
[mcp_servers.docs]
url = "https://example.com/mcp"
bearer_token = "secret"
"#,
)?;
let err = load_global_mcp_servers(codex_home.path())
.await
.expect_err("bearer_token entries should be rejected");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("bearer_token"));
assert!(err.to_string().contains("bearer_token_env_var"));
Ok(())
}
#[tokio::test]
async fn write_global_mcp_servers_serializes_env_sorted() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
@@ -1534,7 +1587,7 @@ ZIG_VAR = "3"
McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token: Some("secret-token".to_string()),
bearer_token_env_var: Some("MCP_TOKEN".to_string()),
},
startup_timeout_sec: Some(Duration::from_secs(2)),
tool_timeout_sec: None,
@@ -1549,7 +1602,7 @@ ZIG_VAR = "3"
serialized,
r#"[mcp_servers.docs]
url = "https://example.com/mcp"
bearer_token = "secret-token"
bearer_token_env_var = "MCP_TOKEN"
startup_timeout_sec = 2.0
"#
);
@@ -1557,9 +1610,12 @@ startup_timeout_sec = 2.0
let loaded = load_global_mcp_servers(codex_home.path()).await?;
let docs = loaded.get("docs").expect("docs entry");
match &docs.transport {
McpServerTransportConfig::StreamableHttp { url, bearer_token } => {
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => {
assert_eq!(url, "https://example.com/mcp");
assert_eq!(bearer_token.as_deref(), Some("secret-token"));
assert_eq!(bearer_token_env_var.as_deref(), Some("MCP_TOKEN"));
}
other => panic!("unexpected transport {other:?}"),
}
@@ -1570,7 +1626,7 @@ startup_timeout_sec = 2.0
McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token: None,
bearer_token_env_var: None,
},
startup_timeout_sec: None,
tool_timeout_sec: None,
@@ -1589,9 +1645,12 @@ url = "https://example.com/mcp"
let loaded = load_global_mcp_servers(codex_home.path()).await?;
let docs = loaded.get("docs").expect("docs entry");
match &docs.transport {
McpServerTransportConfig::StreamableHttp { url, bearer_token } => {
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => {
assert_eq!(url, "https://example.com/mcp");
assert!(bearer_token.is_none());
assert!(bearer_token_env_var.is_none());
}
other => panic!("unexpected transport {other:?}"),
}

View File

@@ -48,6 +48,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
url: Option<String>,
bearer_token: Option<String>,
bearer_token_env_var: Option<String>,
#[serde(default)]
startup_timeout_sec: Option<f64>,
@@ -80,6 +81,19 @@ impl<'de> Deserialize<'de> for McpServerConfig {
)))
}
fn throw_if_bearer_token_set<E>(transport: &str, value: Option<&String>) -> Result<(), E>
where
E: SerdeError,
{
if value.is_none() {
return Ok(());
}
Err(E::custom(format!(
"bearer_token is not supported for {transport}; set bearer_token_env_var or use `codex mcp add <name> --url <url> --with-bearer-token`",
)))
}
let transport = match raw {
RawMcpServerConfig {
command: Some(command),
@@ -87,10 +101,16 @@ impl<'de> Deserialize<'de> for McpServerConfig {
env,
url,
bearer_token,
bearer_token_env_var,
..
} => {
throw_if_set("stdio", "url", url.as_ref())?;
throw_if_set("stdio", "bearer_token", bearer_token.as_ref())?;
throw_if_bearer_token_set("stdio", bearer_token.as_ref())?;
throw_if_set(
"stdio",
"bearer_token_env_var",
bearer_token_env_var.as_ref(),
)?;
McpServerTransportConfig::Stdio {
command,
args: args.unwrap_or_default(),
@@ -100,6 +120,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
RawMcpServerConfig {
url: Some(url),
bearer_token,
bearer_token_env_var,
command,
args,
env,
@@ -108,7 +129,11 @@ impl<'de> Deserialize<'de> for McpServerConfig {
throw_if_set("streamable_http", "command", command.as_ref())?;
throw_if_set("streamable_http", "args", args.as_ref())?;
throw_if_set("streamable_http", "env", env.as_ref())?;
McpServerTransportConfig::StreamableHttp { url, bearer_token }
throw_if_bearer_token_set("streamable_http", bearer_token.as_ref())?;
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
}
}
_ => return Err(SerdeError::custom("invalid transport")),
};
@@ -135,11 +160,11 @@ pub enum McpServerTransportConfig {
/// https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http
StreamableHttp {
url: String,
/// A plain text bearer token to use for authentication.
/// This bearer token will be included in the HTTP request header as an `Authorization: Bearer <token>` header.
/// This should be used with caution because it lives on disk in clear text.
/// Name of the environment variable to read for an HTTP bearer token.
/// When set, requests will include the token via `Authorization: Bearer <token>`.
/// The actual secret value must be provided via the environment.
#[serde(default, skip_serializing_if = "Option::is_none")]
bearer_token: Option<String>,
bearer_token_env_var: Option<String>,
},
}
@@ -506,17 +531,17 @@ mod tests {
cfg.transport,
McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token: None
bearer_token_env_var: None
}
);
}
#[test]
fn deserialize_streamable_http_server_config_with_bearer_token() {
fn deserialize_streamable_http_server_config_with_env_var() {
let cfg: McpServerConfig = toml::from_str(
r#"
url = "https://example.com/mcp"
bearer_token = "secret"
bearer_token_env_var = "GITHUB_TOKEN"
"#,
)
.expect("should deserialize http config");
@@ -525,7 +550,7 @@ mod tests {
cfg.transport,
McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token: Some("secret".to_string())
bearer_token_env_var: Some("GITHUB_TOKEN".to_string())
}
);
}
@@ -553,13 +578,18 @@ mod tests {
}
#[test]
fn deserialize_rejects_bearer_token_for_stdio_transport() {
toml::from_str::<McpServerConfig>(
fn deserialize_rejects_inline_bearer_token_field() {
let err = toml::from_str::<McpServerConfig>(
r#"
command = "echo"
url = "https://example.com"
bearer_token = "secret"
"#,
)
.expect_err("should reject bearer token for stdio transport");
.expect_err("should reject bearer_token field");
assert!(
err.to_string().contains("bearer_token is not supported"),
"unexpected error: {err}"
);
}
}

View File

@@ -8,6 +8,7 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
use std::ffi::OsString;
use std::sync::Arc;
use std::time::Duration;
@@ -205,6 +206,14 @@ impl McpConnectionManager {
let startup_timeout = cfg.startup_timeout_sec.unwrap_or(DEFAULT_STARTUP_TIMEOUT);
let tool_timeout = cfg.tool_timeout_sec.unwrap_or(DEFAULT_TOOL_TIMEOUT);
let resolved_bearer_token = match &cfg.transport {
McpServerTransportConfig::StreamableHttp {
bearer_token_env_var,
..
} => resolve_bearer_token(&server_name, bearer_token_env_var.as_deref())?,
_ => None,
};
join_set.spawn(async move {
let McpServerConfig { transport, .. } = cfg;
let params = mcp_types::InitializeRequestParams {
@@ -242,11 +251,11 @@ impl McpConnectionManager {
)
.await
}
McpServerTransportConfig::StreamableHttp { url, bearer_token } => {
McpServerTransportConfig::StreamableHttp { url, .. } => {
McpClientAdapter::new_streamable_http_client(
server_name.clone(),
url,
bearer_token,
resolved_bearer_token,
params,
startup_timeout,
)
@@ -336,6 +345,33 @@ impl McpConnectionManager {
}
}
fn resolve_bearer_token(
server_name: &str,
bearer_token_env_var: Option<&str>,
) -> Result<Option<String>> {
let Some(env_var) = bearer_token_env_var else {
return Ok(None);
};
match env::var(env_var) {
Ok(value) => {
if value.is_empty() {
Err(anyhow!(
"Environment variable {env_var} for MCP server '{server_name}' is empty"
))
} else {
Ok(Some(value))
}
}
Err(env::VarError::NotPresent) => Err(anyhow!(
"Environment variable {env_var} for MCP server '{server_name}' is not set"
)),
Err(env::VarError::NotUnicode(_)) => Err(anyhow!(
"Environment variable {env_var} for MCP server '{server_name}' contains invalid Unicode"
)),
}
}
/// Query every server for its available tools and return a single map that
/// contains **all** tools. Each key is the fully-qualified name for the tool.
async fn list_all_tools(clients: &HashMap<String, ManagedClient>) -> Result<Vec<ToolInfo>> {

View File

@@ -232,7 +232,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp {
url: server_url,
bearer_token: None,
bearer_token_env_var: None,
},
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,
@@ -412,7 +412,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> {
McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp {
url: server_url,
bearer_token: None,
bearer_token_env_var: None,
},
startup_timeout_sec: Some(Duration::from_secs(10)),
tool_timeout_sec: None,