Add MCP server context to otel tool_result logs (#12267)

Summary
- capture the origin for each configured MCP server and expose it via
the connection manager
- plumb MCP server name/origin into tool logging and emit
codex.tool_result events with those fields
- add unit coverage for origin parsing and extend OTEL tests to assert
empty MCP fields for non-MCP tools
- currently not logging full urls or url paths to prevent logging
potentially sensitive data

Testing
- Not run (not requested)
This commit is contained in:
colby-oai
2026-02-20 10:26:19 -05:00
committed by GitHub
parent ede561b5d1
commit 2036a5f5e0
5 changed files with 148 additions and 0 deletions
@@ -73,6 +73,7 @@ use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
use tracing::warn;
use url::Url;
use crate::codex::INITIAL_SUBMIT_ID;
use crate::config::types::McpServerConfig;
@@ -507,6 +508,7 @@ pub struct SandboxState {
/// A thin wrapper around a set of running [`RmcpClient`] instances.
pub(crate) struct McpConnectionManager {
clients: HashMap<String, AsyncManagedClient>,
server_origins: HashMap<String, String>,
elicitation_requests: ElicitationRequestManager,
}
@@ -514,6 +516,7 @@ impl McpConnectionManager {
pub(crate) fn new_uninitialized(approval_policy: &Constrained<AskForApproval>) -> Self {
Self {
clients: HashMap::new(),
server_origins: HashMap::new(),
elicitation_requests: ElicitationRequestManager::new(approval_policy.value()),
}
}
@@ -529,6 +532,10 @@ impl McpConnectionManager {
!self.clients.is_empty()
}
pub(crate) fn server_origin(&self, server_name: &str) -> Option<&str> {
self.server_origins.get(server_name).map(String::as_str)
}
pub fn set_approval_policy(&self, approval_policy: &Constrained<AskForApproval>) {
if let Ok(mut policy) = self.elicitation_requests.approval_policy.lock() {
*policy = approval_policy.value();
@@ -548,10 +555,14 @@ impl McpConnectionManager {
) -> (Self, CancellationToken) {
let cancel_token = CancellationToken::new();
let mut clients = HashMap::new();
let mut server_origins = HashMap::new();
let mut join_set = JoinSet::new();
let elicitation_requests = ElicitationRequestManager::new(approval_policy.value());
let mcp_servers = mcp_servers.clone();
for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) {
if let Some(origin) = transport_origin(&cfg.transport) {
server_origins.insert(server_name.clone(), origin);
}
let cancel_token = cancel_token.child_token();
let _ = emit_update(
&tx_event,
@@ -624,6 +635,7 @@ impl McpConnectionManager {
}
let manager = Self {
clients,
server_origins,
elicitation_requests: elicitation_requests.clone(),
};
tokio::spawn(async move {
@@ -1448,6 +1460,16 @@ fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &str)]) {
}
}
fn transport_origin(transport: &McpServerTransportConfig) -> Option<String> {
match transport {
McpServerTransportConfig::StreamableHttp { url, .. } => {
let parsed = Url::parse(url).ok()?;
Some(parsed.origin().ascii_serialization())
}
McpServerTransportConfig::Stdio { .. } => Some("stdio".to_string()),
}
}
async fn list_tools_for_client_uncached(
server_name: &str,
client: &Arc<RmcpClient>,
@@ -2164,4 +2186,32 @@ mod tests {
display
);
}
#[test]
fn transport_origin_extracts_http_origin() {
let transport = McpServerTransportConfig::StreamableHttp {
url: "https://example.com:8443/path?query=1".to_string(),
bearer_token_env_var: None,
http_headers: None,
env_http_headers: None,
};
assert_eq!(
transport_origin(&transport),
Some("https://example.com:8443".to_string())
);
}
#[test]
fn transport_origin_is_stdio_for_stdio_transport() {
let transport = McpServerTransportConfig::Stdio {
command: "server".to_string(),
args: Vec::new(),
env: None,
env_vars: Vec::new(),
cwd: None,
};
assert_eq!(transport_origin(&transport), Some("stdio".to_string()));
}
}
+21
View File
@@ -102,6 +102,21 @@ impl ToolRegistry {
sandbox_policy_tag(&invocation.turn.sandbox_policy),
),
];
let (mcp_server, mcp_server_origin) = match &invocation.payload {
ToolPayload::Mcp { server, .. } => {
let manager = invocation
.session
.services
.mcp_connection_manager
.read()
.await;
let origin = manager.server_origin(server).map(str::to_owned);
(Some(server.clone()), origin)
}
_ => (None, None),
};
let mcp_server_ref = mcp_server.as_deref();
let mcp_server_origin_ref = mcp_server_origin.as_deref();
let handler = match self.handler(tool_name.as_ref()) {
Some(handler) => handler,
@@ -116,6 +131,8 @@ impl ToolRegistry {
false,
&message,
&metric_tags,
mcp_server_ref,
mcp_server_origin_ref,
);
return Err(FunctionCallError::RespondToModel(message));
}
@@ -131,6 +148,8 @@ impl ToolRegistry {
false,
&message,
&metric_tags,
mcp_server_ref,
mcp_server_origin_ref,
);
return Err(FunctionCallError::Fatal(message));
}
@@ -146,6 +165,8 @@ impl ToolRegistry {
&call_id_owned,
log_payload.as_ref(),
&metric_tags,
mcp_server_ref,
mcp_server_origin_ref,
|| {
let handler = handler.clone();
let output_cell = &output_cell;