Set a default user agent for MCP HTTP requests (#34883)

## What changed

- Send `codex-mcp-client/<version>` as the default user agent for streamable HTTP and OAuth requests.
- Preserve user agents supplied through configured HTTP headers.

## Testing

- Verify the default user agent on OAuth discovery, token refresh, and MCP initialization requests.
- Verify that a configured user agent overrides the default.

GitOrigin-RevId: 659ef8f126df97b3c1b4d01e9e542a673b5ef42b
This commit is contained in:
Eric Traut
2026-07-23 06:08:18 +00:00
committed by copyberry
parent 4e0cee8030
commit 6e0455fdc4
3 changed files with 86 additions and 0 deletions

View File

@@ -5,10 +5,13 @@ use reqwest::ClientBuilder;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderName;
use reqwest::header::HeaderValue;
use reqwest::header::USER_AGENT;
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
const MCP_USER_AGENT: &str = concat!("codex-mcp-client/", env!("CARGO_PKG_VERSION"));
pub(crate) fn create_env_for_mcp_server(
extra_env: Option<HashMap<OsString, OsString>>,
env_vars: &[McpServerEnvVar],
@@ -62,6 +65,7 @@ pub(crate) fn build_default_headers(
env_http_headers: Option<HashMap<String, String>>,
) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static(MCP_USER_AGENT));
if let Some(static_headers) = http_headers {
for (name, value) in static_headers {

View File

@@ -50,6 +50,10 @@ async fn refreshes_expired_persisted_token_before_initialize() -> anyhow::Result
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/.well-known/oauth-authorization-server/mcp"))
.and(header(
"user-agent",
concat!("codex-mcp-client/", env!("CARGO_PKG_VERSION")),
))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"authorization_endpoint": format!("{}/oauth/authorize", server.uri()),
"token_endpoint": format!("{}/oauth/token", server.uri()),
@@ -60,6 +64,10 @@ async fn refreshes_expired_persisted_token_before_initialize() -> anyhow::Result
.await;
Mock::given(method("POST"))
.and(path("/oauth/token"))
.and(header(
"user-agent",
concat!("codex-mcp-client/", env!("CARGO_PKG_VERSION")),
))
.and(body_string_contains("grant_type=refresh_token"))
.and(body_string_contains(format!(
"refresh_token={REFRESH_TOKEN}"
@@ -75,6 +83,10 @@ async fn refreshes_expired_persisted_token_before_initialize() -> anyhow::Result
.await;
Mock::given(method("POST"))
.and(path("/mcp"))
.and(header(
"user-agent",
concat!("codex-mcp-client/", env!("CARGO_PKG_VERSION")),
))
.and(header(
"authorization",
format!("Bearer {REFRESHED_ACCESS_TOKEN}"),

View File

@@ -0,0 +1,70 @@
mod streamable_http_test_support;
use std::collections::HashMap;
use codex_config::types::AuthKeyringBackendKind;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_exec_server::Environment;
use codex_rmcp_client::RmcpClient;
use serde_json::Value;
use serde_json::json;
use streamable_http_test_support::initialize_client;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::Request;
use wiremock::ResponseTemplate;
use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn streamable_http_requests_preserve_configured_user_agent() -> anyhow::Result<()> {
let custom_user_agent = "custom-agent/9.9";
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/mcp"))
.and(header("user-agent", custom_user_agent))
.respond_with(|request: &Request| {
let body: Value = request.body_json().expect("valid JSON-RPC request");
match body.get("method").and_then(Value::as_str) {
Some("initialize") => ResponseTemplate::new(200).set_body_json(json!({
"jsonrpc": "2.0",
"id": body.get("id").cloned().unwrap_or(Value::Null),
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"serverInfo": {
"name": "user-agent-test",
"version": "0.0.0-test",
},
},
})),
Some("notifications/initialized") => ResponseTemplate::new(202),
method => ResponseTemplate::new(400)
.set_body_string(format!("unexpected JSON-RPC method: {method:?}")),
}
})
.expect(2)
.mount(&server)
.await;
let custom_client = RmcpClient::new_streamable_http_client(
"test-streamable-http",
&format!("{}/mcp", server.uri()),
Some("test-bearer".to_string()),
Some(HashMap::from([(
"user-agent".to_string(),
custom_user_agent.to_string(),
)])),
/*env_http_headers*/ None,
OAuthCredentialsStoreMode::File,
AuthKeyringBackendKind::default(),
Environment::default_for_tests().get_http_client(),
/*auth_provider*/ None,
)
.await?;
initialize_client(&custom_client).await?;
server.verify().await;
Ok(())
}