Enable skills in the MCP server (#36339)

## What changed

- Install the skills extension for MCP Codex tool sessions so host skills are
  included in the developer instructions.
- Route extension warnings to the matching active MCP turn as `codex/event`
  notifications, preserving request and thread metadata and ordering warnings
  before the final tool response.
- Track reply turns independently and bound forwarded warning messages to 256
  UTF-8 bytes.

## Testing

- Add unit coverage for active-turn routing, request ID collisions, warning
  ordering, filtering, and truncation.
- Add an MCP integration test for host skill instructions and skills context
  budget warnings.

GitOrigin-RevId: 9e5c699665f6889bf1103e78c3d6ca53824a10a0
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-07-31 14:40:40 +00:00
1 parent 448118f544
commit 5548c95d66
14 files changed
+734 -205

No files matched your search

@@ -0,0 +1,97 @@
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::MutexGuard;
use std::sync::PoisonError;
use codex_protocol::ThreadId;
use rmcp::model::RequestId;
/// Tracks active MCP requests by both request ID and Codex turn.
///
/// Warning and response enqueue callbacks run while holding the same lock. This
/// makes route removal and response enqueue atomic with respect to warning
/// enqueue, so a warning is either queued before its tool response or dropped.
#[derive(Default)]
pub(crate) struct ActiveTurnRegistry {
state: Mutex<ActiveTurnState>,
}
#[derive(Default)]
struct ActiveTurnState {
by_request: HashMap<RequestId, ActiveTurn>,
by_thread_and_turn: HashMap<(ThreadId, String), RequestId>,
}
struct ActiveTurn {
thread_id: ThreadId,
turn_id: String,
}
impl ActiveTurnRegistry {
pub(crate) fn register(&self, request_id: RequestId, thread_id: ThreadId, turn_id: String) {
let active_turn = ActiveTurn {
thread_id,
turn_id: turn_id.clone(),
};
let mut state = self.lock();
if let Some(previous_turn) = state.by_request.insert(request_id.clone(), active_turn) {
state
.by_thread_and_turn
.remove(&(previous_turn.thread_id, previous_turn.turn_id));
}
state
.by_thread_and_turn
.insert((thread_id, turn_id), request_id);
}
pub(crate) fn thread_id(&self, request_id: &RequestId) -> Option<ThreadId> {
self.lock()
.by_request
.get(request_id)
.map(|active_turn| active_turn.thread_id)
}
pub(crate) fn with_request_id(
&self,
thread_id: ThreadId,
turn_id: &str,
f: impl FnOnce(&RequestId),
) -> bool {
let state = self.lock();
let Some(request_id) = state
.by_thread_and_turn
.get(&(thread_id, turn_id.to_string()))
else {
return false;
};
f(request_id);
true
}
pub(crate) fn unregister(&self, request_id: &RequestId) {
let mut state = self.lock();
Self::remove(&mut state, request_id);
}
pub(crate) fn finish(&self, request_id: &RequestId, finish: impl FnOnce()) {
let mut state = self.lock();
Self::remove(&mut state, request_id);
finish();
}
fn remove(state: &mut ActiveTurnState, request_id: &RequestId) {
if let Some(active_turn) = state.by_request.remove(request_id) {
state
.by_thread_and_turn
.remove(&(active_turn.thread_id, active_turn.turn_id));
}
}
fn lock(&self) -> MutexGuard<'_, ActiveTurnState> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
}
#[cfg(test)]
#[path = "active_turn_registry_tests.rs"]
mod tests;
@@ -0,0 +1,70 @@
use codex_protocol::ThreadId;
use pretty_assertions::assert_eq;
use rmcp::model::RequestId;
use super::ActiveTurnRegistry;
#[test]
fn colliding_request_id_strings_use_independent_turn_routes() {
let active_turns = ActiveTurnRegistry::default();
let thread_id = ThreadId::new();
let numeric_request_id = RequestId::Number(7);
let string_request_id = RequestId::String("7".into());
let numeric_turn_id = "numeric-turn".to_string();
let string_turn_id = "string-turn".to_string();
active_turns.register(
numeric_request_id.clone(),
thread_id,
numeric_turn_id.clone(),
);
active_turns.register(string_request_id.clone(), thread_id, string_turn_id.clone());
let mut routed_request_ids = Vec::new();
assert!(
active_turns.with_request_id(thread_id, &numeric_turn_id, |request_id| {
routed_request_ids.push(request_id.clone());
})
);
assert!(
active_turns.with_request_id(thread_id, &string_turn_id, |request_id| {
routed_request_ids.push(request_id.clone());
})
);
assert_eq!(
routed_request_ids,
vec![numeric_request_id.clone(), string_request_id.clone()]
);
active_turns.unregister(&numeric_request_id);
assert!(!active_turns.with_request_id(thread_id, &numeric_turn_id, |_| {}));
let mut remaining_request_id = None;
assert!(
active_turns.with_request_id(thread_id, &string_turn_id, |request_id| {
remaining_request_id = Some(request_id.clone());
})
);
assert_eq!(remaining_request_id, Some(string_request_id));
}
#[test]
fn reused_request_id_replaces_the_previous_turn_route() {
let active_turns = ActiveTurnRegistry::default();
let thread_id = ThreadId::new();
let request_id = RequestId::Number(7);
let first_turn_id = "first-turn".to_string();
let second_turn_id = "second-turn".to_string();
active_turns.register(request_id.clone(), thread_id, first_turn_id.clone());
active_turns.register(request_id.clone(), thread_id, second_turn_id.clone());
assert!(!active_turns.with_request_id(thread_id, &first_turn_id, |_| {}));
let mut routed_request_id = None;
assert!(
active_turns.with_request_id(thread_id, &second_turn_id, |request_id| {
routed_request_id = Some(request_id.clone());
})
);
assert_eq!(routed_request_id, Some(request_id));
}
+60 -80
View File
@@ -2,9 +2,9 @@
//! Tokio task. Separated from `message_processor.rs` to keep that file small
//! and to make future feature-growth easier to manage.
use std::collections::HashMap;
use std::sync::Arc;
use crate::active_turn_registry::ActiveTurnRegistry;
use crate::exec_approval::handle_exec_approval_request;
use crate::outgoing_message::OutgoingMessageSender;
use crate::outgoing_message::OutgoingNotificationMeta;
@@ -28,7 +28,7 @@ use rmcp::model::CallToolResult;
use rmcp::model::ContentBlock;
use rmcp::model::RequestId;
use serde_json::json;
use tokio::sync::Mutex;
use uuid::Uuid;
/// To adhere to MCP `tools/call` response format, include the Codex
/// `threadId` in the `structured_content` field of the response.
@@ -61,7 +61,7 @@ pub async fn run_codex_tool_session(
config: CodexConfig,
outgoing: Arc<OutgoingMessageSender>,
thread_manager: Arc<ThreadManager>,
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, ThreadId>>>,
active_turns: Arc<ActiveTurnRegistry>,
) {
let NewThread {
thread_id,
@@ -76,7 +76,7 @@ pub async fn run_codex_tool_session(
let result = CallToolResult::error(vec![ContentBlock::text(format!(
"Failed to start Codex session: {e}"
))]);
outgoing.send_response(id.clone(), result).await;
outgoing.send_response(id.clone(), result);
return;
}
};
@@ -86,26 +86,20 @@ pub async fn run_codex_tool_session(
id: "".to_string(),
msg: EventMsg::SessionConfigured(session_configured.clone()),
};
outgoing
.send_event_as_notification(
&session_configured_event,
Some(OutgoingNotificationMeta {
request_id: Some(id.clone()),
thread_id: Some(thread_id),
}),
)
.await;
outgoing.send_event_as_notification(
&session_configured_event,
Some(OutgoingNotificationMeta {
request_id: Some(id.clone()),
thread_id: Some(thread_id),
}),
);
// Use the original MCP request ID as the `sub_id` for the Codex submission so that
// any events emitted for this tool-call can be correlated with the
// originating `tools/call` request.
let sub_id = id.to_string();
running_requests_id_to_codex_uuid
.lock()
.await
.insert(id.clone(), thread_id);
// Preserve the legacy event ID for initial `codex` calls. Each call starts
// a new thread, so the thread and turn pair remains unique.
let turn_id = id.to_string();
active_turns.register(id.clone(), thread_id, turn_id.clone());
let submission = Submission {
id: sub_id.clone(),
id: turn_id,
op: Op::UserInput {
items: vec![UserInput::Text {
text: initial_prompt.clone(),
@@ -129,20 +123,11 @@ pub async fn run_codex_tool_session(
format!("Failed to submit initial prompt: {e}"),
Some(true),
);
outgoing.send_response(id.clone(), result).await;
// unregister the id so we don't keep it in the map
running_requests_id_to_codex_uuid.lock().await.remove(&id);
active_turns.finish(&id, || outgoing.send_response(id.clone(), result));
return;
}
run_codex_tool_session_inner(
thread_id,
thread,
outgoing,
id,
running_requests_id_to_codex_uuid,
)
.await;
run_codex_tool_session_inner(thread_id, thread, outgoing, id, active_turns).await;
}
pub async fn run_codex_tool_session_reply(
@@ -151,23 +136,29 @@ pub async fn run_codex_tool_session_reply(
outgoing: Arc<OutgoingMessageSender>,
request_id: RequestId,
prompt: String,
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, ThreadId>>>,
active_turns: Arc<ActiveTurnRegistry>,
) {
running_requests_id_to_codex_uuid
.lock()
.await
.insert(request_id.clone(), thread_id);
// Replies share a thread, so use Core's UUIDv7 submission ID convention
// instead of a reusable MCP request ID.
let turn_id = Uuid::now_v7().to_string();
active_turns.register(request_id.clone(), thread_id, turn_id.clone());
if let Err(e) = thread
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: prompt,
// MCP tool prompts are plain text with no UI element ranges.
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
.submit_with_id(Submission {
id: turn_id,
op: Op::UserInput {
items: vec![UserInput::Text {
text: prompt,
// MCP tool prompts are plain text with no UI element ranges.
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
},
client_user_message_id: None,
trace: None,
parent_turn_id: None,
})
.await
{
@@ -177,23 +168,13 @@ pub async fn run_codex_tool_session_reply(
format!("Failed to submit user input: {e}"),
Some(true),
);
outgoing.send_response(request_id.clone(), result).await;
// unregister the id so we don't keep it in the map
running_requests_id_to_codex_uuid
.lock()
.await
.remove(&request_id);
active_turns.finish(&request_id, || {
outgoing.send_response(request_id.clone(), result);
});
return;
}
run_codex_tool_session_inner(
thread_id,
thread,
outgoing,
request_id,
running_requests_id_to_codex_uuid,
)
.await;
run_codex_tool_session_inner(thread_id, thread, outgoing, request_id, active_turns).await;
}
async fn run_codex_tool_session_inner(
@@ -201,7 +182,7 @@ async fn run_codex_tool_session_inner(
thread: Arc<CodexThread>,
outgoing: Arc<OutgoingMessageSender>,
request_id: RequestId,
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, ThreadId>>>,
active_turns: Arc<ActiveTurnRegistry>,
) {
let request_id_str = request_id.to_string();
@@ -210,15 +191,13 @@ async fn run_codex_tool_session_inner(
loop {
match thread.next_event().await {
Ok(event) => {
outgoing
.send_event_as_notification(
&event,
Some(OutgoingNotificationMeta {
request_id: Some(request_id.clone()),
thread_id: Some(thread_id),
}),
)
.await;
outgoing.send_event_as_notification(
&event,
Some(OutgoingNotificationMeta {
request_id: Some(request_id.clone()),
thread_id: Some(thread_id),
}),
);
match event.msg {
EventMsg::ExecApprovalRequest(ev) => {
@@ -267,7 +246,9 @@ async fn run_codex_tool_session_inner(
err_event.message,
Some(true),
);
outgoing.send_response(request_id.clone(), result).await;
active_turns.finish(&request_id, || {
outgoing.send_response(request_id.clone(), result);
});
break;
}
EventMsg::Warning(_)
@@ -317,12 +298,9 @@ async fn run_codex_tool_session_inner(
let result = create_call_tool_result_with_thread_id(
thread_id, text, /*is_error*/ None,
);
outgoing.send_response(request_id.clone(), result).await;
// unregister the id so we don't keep it in the map
running_requests_id_to_codex_uuid
.lock()
.await
.remove(&request_id);
active_turns.finish(&request_id, || {
outgoing.send_response(request_id.clone(), result);
});
break;
}
EventMsg::SessionConfigured(_) => {
@@ -415,7 +393,9 @@ async fn run_codex_tool_session_inner(
format!("Codex runtime error: {e}"),
Some(true),
);
outgoing.send_response(request_id.clone(), result).await;
active_turns.finish(&request_id, || {
outgoing.send_response(request_id.clone(), result);
});
break;
}
}
+1 -3
View File
@@ -86,9 +86,7 @@ pub(crate) async fn handle_exec_approval_request(
let message = format!("Failed to serialize ExecApprovalElicitRequestParams: {err}");
error!("{message}");
outgoing
.send_error(request_id.clone(), ErrorData::invalid_params(message, None))
.await;
outgoing.send_error(request_id.clone(), ErrorData::invalid_params(message, None));
return;
}
@@ -0,0 +1,89 @@
use std::sync::Arc;
use codex_extension_api::ExtensionEventSink;
use codex_extension_api::ExtensionWarning;
use codex_protocol::ThreadId;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::WarningEvent;
use crate::active_turn_registry::ActiveTurnRegistry;
use crate::outgoing_message::OutgoingMessageSender;
use crate::outgoing_message::OutgoingNotificationMeta;
const MAX_EXTENSION_WARNING_BYTES: usize = 256;
pub(crate) fn extension_event_sink(
outgoing: Arc<OutgoingMessageSender>,
active_turns: Arc<ActiveTurnRegistry>,
) -> Arc<dyn ExtensionEventSink> {
Arc::new(McpExtensionEventSink {
outgoing,
active_turns,
})
}
struct McpExtensionEventSink {
outgoing: Arc<OutgoingMessageSender>,
active_turns: Arc<ActiveTurnRegistry>,
}
impl ExtensionEventSink for McpExtensionEventSink {
fn emit(&self, event: Event) {
tracing::debug!(
event_id = %event.id,
msg = ?event.msg,
"dropping unsupported extension event"
);
}
fn emit_warning(&self, warning: ExtensionWarning) {
let ExtensionWarning {
thread_id,
turn_id,
message,
} = warning;
let Ok(thread_id) = ThreadId::from_string(&thread_id) else {
tracing::warn!(%thread_id, "dropping extension warning with invalid thread id");
return;
};
let Some(turn_id) = turn_id else {
tracing::debug!(%thread_id, "dropping extension warning without a turn id");
return;
};
let mut message = message;
if message.len() > MAX_EXTENSION_WARNING_BYTES {
let mut truncate_at = MAX_EXTENSION_WARNING_BYTES;
while !message.is_char_boundary(truncate_at) {
truncate_at -= 1;
}
message.truncate(truncate_at);
}
let found_active_turn =
self.active_turns
.with_request_id(thread_id, &turn_id, |request_id| {
let event = Event {
id: turn_id.clone(),
msg: EventMsg::Warning(WarningEvent { message }),
};
self.outgoing.send_event_as_notification(
&event,
Some(OutgoingNotificationMeta {
request_id: Some(request_id.clone()),
thread_id: Some(thread_id),
}),
);
});
if !found_active_turn {
tracing::debug!(
%thread_id,
%turn_id,
"dropping extension warning without a matching active turn"
);
}
}
}
#[cfg(test)]
#[path = "extension_event_sink_tests.rs"]
mod tests;
@@ -0,0 +1,190 @@
use std::sync::Arc;
use codex_extension_api::ExtensionEventSink;
use codex_extension_api::ExtensionWarning;
use codex_protocol::ThreadId;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::WarningEvent;
use pretty_assertions::assert_eq;
use rmcp::model::RequestId;
use serde_json::json;
use tokio::sync::mpsc;
use super::extension_event_sink;
use crate::active_turn_registry::ActiveTurnRegistry;
use crate::outgoing_message::OutgoingMessage;
use crate::outgoing_message::OutgoingMessageSender;
use crate::outgoing_message::OutgoingNotification;
fn test_sink() -> (
Arc<dyn ExtensionEventSink>,
Arc<OutgoingMessageSender>,
Arc<ActiveTurnRegistry>,
mpsc::UnboundedReceiver<OutgoingMessage>,
) {
let (outgoing_tx, outgoing_rx) = mpsc::unbounded_channel();
let outgoing = Arc::new(OutgoingMessageSender::new(outgoing_tx));
let active_turns = Arc::new(ActiveTurnRegistry::default());
(
extension_event_sink(Arc::clone(&outgoing), Arc::clone(&active_turns)),
outgoing,
active_turns,
outgoing_rx,
)
}
#[tokio::test(flavor = "current_thread")]
async fn warning_is_enqueued_before_following_response() {
let (sink, outgoing, active_turns, mut outgoing_rx) = test_sink();
let request_id = RequestId::Number(7);
let thread_id = ThreadId::new();
let turn_id = "turn-7".to_string();
active_turns.register(request_id.clone(), thread_id, turn_id.clone());
sink.emit_warning(ExtensionWarning {
thread_id: thread_id.to_string(),
turn_id: Some(turn_id),
message: "warning".to_string(),
});
active_turns.finish(&request_id, || {
outgoing.send_response(request_id.clone(), json!({}));
});
let Some(OutgoingMessage::Notification(notification)) = outgoing_rx.recv().await else {
panic!("warning notification should be enqueued before the response");
};
assert_eq!(notification.method, "codex/event");
assert_eq!(
notification.params.as_ref().map(|params| &params["msg"]),
Some(&json!({
"message": "warning",
"type": "warning",
}))
);
}
#[tokio::test(flavor = "current_thread")]
async fn colliding_request_id_strings_route_warnings_independently() {
let (sink, _outgoing, active_turns, mut outgoing_rx) = test_sink();
let thread_id = ThreadId::new();
let numeric_turn_id = "019c0000-0000-7000-8000-000000000001".to_string();
let string_turn_id = "019c0000-0000-7000-8000-000000000002".to_string();
active_turns.register(RequestId::Number(7), thread_id, numeric_turn_id.clone());
active_turns.register(
RequestId::String("7".into()),
thread_id,
string_turn_id.clone(),
);
sink.emit_warning(ExtensionWarning {
thread_id: thread_id.to_string(),
turn_id: Some(numeric_turn_id.clone()),
message: "numeric warning".to_string(),
});
sink.emit_warning(ExtensionWarning {
thread_id: thread_id.to_string(),
turn_id: Some(string_turn_id.clone()),
message: "string warning".to_string(),
});
let Some(OutgoingMessage::Notification(numeric_warning)) = outgoing_rx.recv().await else {
panic!("expected warning for numeric request id");
};
assert_eq!(
numeric_warning,
OutgoingNotification {
method: "codex/event".to_string(),
params: Some(json!({
"_meta": {
"requestId": 7,
"threadId": thread_id,
},
"id": numeric_turn_id,
"msg": {
"message": "numeric warning",
"type": "warning",
},
})),
}
);
let Some(OutgoingMessage::Notification(string_warning)) = outgoing_rx.recv().await else {
panic!("expected warning for string request id");
};
assert_eq!(
string_warning,
OutgoingNotification {
method: "codex/event".to_string(),
params: Some(json!({
"_meta": {
"requestId": "7",
"threadId": thread_id,
},
"id": string_turn_id,
"msg": {
"message": "string warning",
"type": "warning",
},
})),
}
);
}
#[tokio::test(flavor = "current_thread")]
async fn warning_requires_a_matching_active_turn() {
let (sink, _outgoing, active_turns, mut outgoing_rx) = test_sink();
let request_id = RequestId::Number(7);
let thread_id = ThreadId::new();
active_turns.register(request_id, thread_id, "turn-7".to_string());
sink.emit_warning(ExtensionWarning {
thread_id: thread_id.to_string(),
turn_id: Some("different-turn".to_string()),
message: "warning".to_string(),
});
tokio::task::yield_now().await;
assert!(outgoing_rx.try_recv().is_err());
}
#[tokio::test(flavor = "current_thread")]
async fn warning_is_truncated_to_256_utf8_bytes() {
let (sink, _outgoing, active_turns, mut outgoing_rx) = test_sink();
let request_id = RequestId::Number(7);
let thread_id = ThreadId::new();
let turn_id = "turn-7".to_string();
active_turns.register(request_id.clone(), thread_id, turn_id.clone());
sink.emit_warning(ExtensionWarning {
thread_id: thread_id.to_string(),
turn_id: Some(turn_id),
message: format!("{}é", "a".repeat(255)),
});
let Some(OutgoingMessage::Notification(notification)) = outgoing_rx.recv().await else {
panic!("expected warning notification");
};
assert_eq!(
notification.params.as_ref().map(|params| &params["msg"]),
Some(&json!({
"message": "a".repeat(255),
"type": "warning",
}))
);
}
#[tokio::test(flavor = "current_thread")]
async fn generic_extension_events_are_dropped() {
let (sink, _outgoing, _running_requests, mut outgoing_rx) = test_sink();
sink.emit(Event {
id: "turn".to_string(),
msg: EventMsg::Warning(WarningEvent {
message: "warning".to_string(),
}),
});
tokio::task::yield_now().await;
assert!(outgoing_rx.try_recv().is_err());
}
+2
View File
@@ -29,9 +29,11 @@ use tracing::info;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
mod active_turn_registry;
mod codex_tool_config;
mod codex_tool_runner;
mod exec_approval;
mod extension_event_sink;
pub(crate) mod message_processor;
mod outgoing_message;
mod patch_approval;
+83 -82
View File
@@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::sync::Arc;
use codex_arg0::Arg0DispatchPaths;
@@ -11,7 +10,6 @@ use codex_home::CodexHomeUserInstructionsProvider;
use codex_login::AuthManager;
use codex_login::default_client::USER_AGENT_SUFFIX;
use codex_login::default_client::get_codex_user_agent;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::Submission;
use rmcp::model::CallToolRequestParams;
@@ -29,13 +27,14 @@ use rmcp::model::JsonRpcResponse;
use rmcp::model::RequestId;
use rmcp::model::ServerCapabilities;
use serde_json::json;
use tokio::sync::Mutex;
use tokio::task;
use crate::active_turn_registry::ActiveTurnRegistry;
use crate::codex_tool_config::CodexToolCallParam;
use crate::codex_tool_config::CodexToolCallReplyParam;
use crate::codex_tool_config::create_tool_for_codex_tool_call_param;
use crate::codex_tool_config::create_tool_for_codex_tool_call_reply_param;
use crate::extension_event_sink::extension_event_sink;
use crate::outgoing_message::OutgoingMessageSender;
pub(crate) struct MessageProcessor {
@@ -43,7 +42,7 @@ pub(crate) struct MessageProcessor {
initialized: bool,
arg0_paths: Arg0DispatchPaths,
thread_manager: Arc<ThreadManager>,
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, ThreadId>>>,
active_turns: Arc<ActiveTurnRegistry>,
}
impl MessageProcessor {
@@ -66,7 +65,10 @@ impl MessageProcessor {
let user_instructions_provider = Arc::new(CodexHomeUserInstructionsProvider::new(
config.codex_home.clone(),
));
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
let active_turns = Arc::new(ActiveTurnRegistry::default());
let mut extensions = ExtensionRegistryBuilder::<Config>::with_event_sink(
extension_event_sink(Arc::clone(&outgoing), Arc::clone(&active_turns)),
);
codex_git_attribution::install(
&mut extensions,
auth_manager.clone(),
@@ -78,6 +80,21 @@ impl MessageProcessor {
auth_manager.clone(),
|config: &Config| Some(config.codex_home.clone()),
);
let skill_providers = codex_skills_extension::SkillProviders::new()
.with_host_provider(Arc::new(codex_skills_extension::HostSkillProvider::new()));
codex_skills_extension::install_with_providers_and_metrics(
&mut extensions,
skill_providers,
codex_otel::global(),
|config: &Config| codex_skills_extension::SkillsExtensionConfig {
include_instructions: config.include_skill_instructions,
bundled_skills_enabled: config.bundled_skills_enabled(),
orchestrator_skills_enabled: config.orchestrator_skills_enabled,
shadow_selection_enabled: config
.features
.enabled(codex_features::Feature::SkillSearch),
},
);
let thread_manager = Arc::new(ThreadManager::new(
config.as_ref(),
Arc::clone(&auth_manager),
@@ -99,7 +116,7 @@ impl MessageProcessor {
initialized: false,
arg0_paths,
thread_manager,
running_requests_id_to_codex_uuid: Arc::new(Mutex::new(HashMap::new())),
active_turns,
}
}
@@ -156,17 +173,15 @@ impl MessageProcessor {
.await;
}
ClientRequest::CustomRequest(custom) => {
let method = custom.method.clone();
self.outgoing
.send_error(
request_id,
ErrorData::new(
ErrorCode::METHOD_NOT_FOUND,
format!("method not found: {method}"),
Some(json!({ "method": method })),
),
)
.await;
let method = custom.method;
self.outgoing.send_error(
request_id,
ErrorData::new(
ErrorCode::METHOD_NOT_FOUND,
format!("method not found: {method}"),
Some(json!({ "method": method })),
),
);
}
request => {
self.handle_unsupported_request(request_id, request.method())
@@ -219,12 +234,10 @@ impl MessageProcessor {
tracing::info!("initialize -> params: {:?}", params);
if self.initialized {
self.outgoing
.send_error(
id,
ErrorData::invalid_request("initialize called more than once", None),
)
.await;
self.outgoing.send_error(
id,
ErrorData::invalid_request("initialize called more than once", None),
);
return;
}
@@ -243,15 +256,13 @@ impl MessageProcessor {
let mut server_info_value = match serde_json::to_value(&server_info) {
Ok(value) => value,
Err(err) => {
self.outgoing
.send_error(
id,
ErrorData::internal_error(
format!("failed to serialize server info: {err}"),
None,
),
)
.await;
self.outgoing.send_error(
id,
ErrorData::internal_error(
format!("failed to serialize server info: {err}"),
None,
),
);
return;
}
};
@@ -264,20 +275,18 @@ impl MessageProcessor {
.enable_tool_list_changed()
.build();
let result = InitializeResult::new(capabilities)
.with_protocol_version(params.protocol_version.clone())
.with_protocol_version(params.protocol_version)
.with_server_info(server_info);
let mut result_value = match serde_json::to_value(result) {
Ok(value) => value,
Err(err) => {
self.outgoing
.send_error(
id,
ErrorData::internal_error(
format!("failed to serialize initialize response: {err}"),
None,
),
)
.await;
self.outgoing.send_error(
id,
ErrorData::internal_error(
format!("failed to serialize initialize response: {err}"),
None,
),
);
return;
}
};
@@ -287,12 +296,12 @@ impl MessageProcessor {
}
self.initialized = true;
self.outgoing.send_response(id, result_value).await;
self.outgoing.send_response(id, result_value);
}
async fn handle_ping(&self, id: RequestId) {
tracing::info!("ping");
self.outgoing.send_response(id, json!({})).await;
self.outgoing.send_response(id, json!({}));
}
fn handle_list_resources(&self, params: Option<rmcp::model::PaginatedRequestParams>) {
@@ -334,7 +343,7 @@ impl MessageProcessor {
create_tool_for_codex_tool_call_reply_param(),
]);
self.outgoing.send_response(id, result).await;
self.outgoing.send_response(id, result);
}
async fn handle_call_tool(&self, id: RequestId, params: CallToolRequestParams) {
@@ -353,7 +362,7 @@ impl MessageProcessor {
let result = CallToolResult::error(vec![rmcp::model::ContentBlock::text(format!(
"Unknown tool '{name}'"
))]);
self.outgoing.send_response(id, result).await;
self.outgoing.send_response(id, result);
}
}
}
@@ -372,7 +381,7 @@ impl MessageProcessor {
let result = CallToolResult::error(vec![rmcp::model::ContentBlock::text(
format!("Failed to load Codex configuration from overrides: {e}"),
)]);
self.outgoing.send_response(id, result).await;
self.outgoing.send_response(id, result);
return;
}
},
@@ -380,7 +389,7 @@ impl MessageProcessor {
let result = CallToolResult::error(vec![rmcp::model::ContentBlock::text(
format!("Failed to parse configuration for Codex tool: {e}"),
)]);
self.outgoing.send_response(id, result).await;
self.outgoing.send_response(id, result);
return;
}
},
@@ -388,7 +397,7 @@ impl MessageProcessor {
let result = CallToolResult::error(vec![rmcp::model::ContentBlock::text(
"Missing arguments for codex tool-call; the `prompt` field is required.",
)]);
self.outgoing.send_response(id, result).await;
self.outgoing.send_response(id, result);
return;
}
};
@@ -396,7 +405,7 @@ impl MessageProcessor {
// Clone outgoing and server to move into async task.
let outgoing = self.outgoing.clone();
let thread_manager = self.thread_manager.clone();
let running_requests_id_to_codex_uuid = self.running_requests_id_to_codex_uuid.clone();
let active_turns = Arc::clone(&self.active_turns);
// Spawn an async task to handle the Codex session so that we do not
// block the synchronous message-processing loop.
@@ -408,7 +417,7 @@ impl MessageProcessor {
config,
outgoing,
thread_manager,
running_requests_id_to_codex_uuid,
active_turns,
)
.await;
});
@@ -431,7 +440,7 @@ impl MessageProcessor {
let result = CallToolResult::error(vec![rmcp::model::ContentBlock::text(
format!("Failed to parse configuration for Codex tool: {e}"),
)]);
self.outgoing.send_response(request_id, result).await;
self.outgoing.send_response(request_id, result);
return;
}
},
@@ -442,7 +451,7 @@ impl MessageProcessor {
let result = CallToolResult::error(vec![rmcp::model::ContentBlock::text(
"Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required.",
)]);
self.outgoing.send_response(request_id, result).await;
self.outgoing.send_response(request_id, result);
return;
}
};
@@ -454,14 +463,14 @@ impl MessageProcessor {
let result = CallToolResult::error(vec![rmcp::model::ContentBlock::text(format!(
"Failed to parse thread_id: {e}"
))]);
self.outgoing.send_response(request_id, result).await;
self.outgoing.send_response(request_id, result);
return;
}
};
// Clone outgoing to move into async task.
let outgoing = self.outgoing.clone();
let running_requests_id_to_codex_uuid = self.running_requests_id_to_codex_uuid.clone();
let active_turns = Arc::clone(&self.active_turns);
let codex = match self.thread_manager.get_thread(thread_id).await {
Ok(c) => c,
@@ -472,7 +481,7 @@ impl MessageProcessor {
format!("Session not found for thread_id: {thread_id}"),
Some(true),
);
outgoing.send_response(request_id, result).await;
outgoing.send_response(request_id, result);
return;
}
};
@@ -481,7 +490,7 @@ impl MessageProcessor {
let prompt = codex_tool_call_reply_param.prompt.clone();
tokio::spawn({
let outgoing = outgoing.clone();
let running_requests_id_to_codex_uuid = running_requests_id_to_codex_uuid.clone();
let active_turns = Arc::clone(&active_turns);
async move {
crate::codex_tool_runner::run_codex_tool_session_reply(
@@ -490,7 +499,7 @@ impl MessageProcessor {
outgoing,
request_id,
prompt,
running_requests_id_to_codex_uuid,
active_turns,
)
.await;
}
@@ -507,16 +516,14 @@ impl MessageProcessor {
}
async fn handle_unsupported_request(&self, id: RequestId, method: &str) {
self.outgoing
.send_error(
id,
ErrorData::new(
ErrorCode::METHOD_NOT_FOUND,
format!("method not found: {method}"),
Some(json!({ "method": method })),
),
)
.await;
self.outgoing.send_error(
id,
ErrorData::new(
ErrorCode::METHOD_NOT_FOUND,
format!("method not found: {method}"),
Some(json!({ "method": method })),
),
);
}
// ---------------------------------------------------------------------
@@ -531,15 +538,12 @@ impl MessageProcessor {
// Create a stable string form early for logging and submission id.
let request_id_string = request_id.to_string();
// Obtain the thread id while holding the first lock, then release.
let thread_id = {
let map_guard = self.running_requests_id_to_codex_uuid.lock().await;
match map_guard.get(&request_id) {
Some(id) => *id,
None => {
tracing::warn!("Session not found for request_id: {request_id_string}");
return;
}
// Resolve the thread for the active MCP request.
let thread_id = match self.active_turns.thread_id(&request_id) {
Some(id) => id,
None => {
tracing::warn!("Session not found for request_id: {request_id_string}");
return;
}
};
tracing::info!("thread_id: {thread_id}");
@@ -567,11 +571,8 @@ impl MessageProcessor {
tracing::error!("Failed to submit interrupt to Codex: {e}");
return;
}
// unregister the id so we don't keep it in the map
self.running_requests_id_to_codex_uuid
.lock()
.await
.remove(&request_id);
// Stop routing extension events to the cancelled turn.
self.active_turns.unregister(&request_id);
}
fn handle_progress_notification(&self, params: rmcp::model::ProgressNotificationParam) {
+14 -24
View File
@@ -79,15 +79,14 @@ impl OutgoingMessageSender {
}
}
pub(crate) async fn send_response<T: Serialize>(&self, id: RequestId, response: T) {
pub(crate) fn send_response<T: Serialize>(&self, id: RequestId, response: T) {
let mut result = match serde_json::to_value(response) {
Ok(result) => result,
Err(err) => {
self.send_error(
id,
ErrorData::internal_error(format!("failed to serialize response: {err}"), None),
)
.await;
);
return;
}
};
@@ -109,7 +108,7 @@ impl OutgoingMessageSender {
/// This is used with the MCP server, but not the more general JSON-RPC app
/// server. Prefer [`OutgoingMessageSender::send_server_notification`] where
/// possible.
pub(crate) async fn send_event_as_notification(
pub(crate) fn send_event_as_notification(
&self,
event: &Event,
meta: Option<OutgoingNotificationMeta>,
@@ -129,17 +128,16 @@ impl OutgoingMessageSender {
self.send_notification(OutgoingNotification {
method: "codex/event".to_string(),
params: Some(params.clone()),
})
.await;
params: Some(params),
});
}
pub(crate) async fn send_notification(&self, notification: OutgoingNotification) {
pub(crate) fn send_notification(&self, notification: OutgoingNotification) {
let outgoing_message = OutgoingMessage::Notification(notification);
let _ = self.sender.send(outgoing_message);
}
pub(crate) async fn send_error(&self, id: RequestId, error: ErrorData) {
pub(crate) fn send_error(&self, id: RequestId, error: ErrorData) {
let outgoing_message = OutgoingMessage::Error(OutgoingError { id, error });
let _ = self.sender.send(outgoing_message);
}
@@ -301,12 +299,10 @@ mod tests {
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::<OutgoingMessage>();
let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx);
outgoing_message_sender
.send_response(
RequestId::Number(1),
rmcp::model::CallToolResult::success(Vec::new()),
)
.await;
outgoing_message_sender.send_response(
RequestId::Number(1),
rmcp::model::CallToolResult::success(Vec::new()),
);
let Some(OutgoingMessage::Response(response)) = outgoing_rx.recv().await else {
panic!("expected a tool-call response");
@@ -346,9 +342,7 @@ mod tests {
}),
};
outgoing_message_sender
.send_event_as_notification(&event, /*meta*/ None)
.await;
outgoing_message_sender.send_event_as_notification(&event, /*meta*/ None);
let result = outgoing_rx.recv().await.unwrap();
let OutgoingMessage::Notification(OutgoingNotification { method, params }) = result else {
@@ -399,9 +393,7 @@ mod tests {
thread_id: None,
};
outgoing_message_sender
.send_event_as_notification(&event, Some(meta))
.await;
outgoing_message_sender.send_event_as_notification(&event, Some(meta));
let result = outgoing_rx.recv().await.unwrap();
let OutgoingMessage::Notification(OutgoingNotification { method, params }) = result else {
@@ -467,9 +459,7 @@ mod tests {
thread_id: Some(thread_id),
};
outgoing_message_sender
.send_event_as_notification(&event, Some(meta))
.await;
outgoing_message_sender.send_event_as_notification(&event, Some(meta));
let result = outgoing_rx.recv().await.unwrap();
let OutgoingMessage::Notification(OutgoingNotification { method, params }) = result else {
+1 -3
View File
@@ -78,9 +78,7 @@ pub(crate) async fn handle_patch_approval_request(
let message = format!("Failed to serialize PatchApprovalElicitRequestParams: {err}");
error!("{message}");
outgoing
.send_error(request_id.clone(), ErrorData::invalid_params(message, None))
.await;
outgoing.send_error(request_id.clone(), ErrorData::invalid_params(message, None));
return;
}