Files
codex/prs/bolinfest/PR-1684.md
2025-09-02 15:17:45 -07:00

331 lines
15 KiB
Markdown

# PR #1684: Changing method in MCP notifications
- URL: https://github.com/openai/codex/pull/1684
- Author: aibrahim-oai
- Created: 2025-07-26 00:11:20 UTC
- Updated: 2025-07-27 03:43:38 UTC
- Changes: +50/-12, Files changed: 3, Commits: 12
## Description
- Changing the codex/event type
## Full Diff
```diff
diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs
index cf6e8b5191..1a6313db92 100644
--- a/codex-rs/core/src/protocol.rs
+++ b/codex-rs/core/src/protocol.rs
@@ -278,8 +278,9 @@ pub struct Event {
}
/// Response event from the agent
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(Debug, Clone, Deserialize, Serialize, Display)]
#[serde(tag = "type", rename_all = "snake_case")]
+#[strum(serialize_all = "lowercase")]
pub enum EventMsg {
/// Error while executing a submission
Error(ErrorEvent),
diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs
index a1eea65f25..e4af1f78cd 100644
--- a/codex-rs/mcp-server/src/outgoing_message.rs
+++ b/codex-rs/mcp-server/src/outgoing_message.rs
@@ -83,11 +83,26 @@ impl OutgoingMessageSender {
let params = Some(serde_json::to_value(event).expect("Event must serialize"));
let outgoing_message = OutgoingMessage::Notification(OutgoingNotification {
method: "codex/event".to_string(),
+ params: params.clone(),
+ });
+ let _ = self.sender.send(outgoing_message).await;
+
+ self.send_event_as_notification_new_schema(event, params)
+ .await;
+ }
+ // should be backwards compatible.
+ // it will replace send_event_as_notification eventually.
+ async fn send_event_as_notification_new_schema(
+ &self,
+ event: &Event,
+ params: Option<serde_json::Value>,
+ ) {
+ let outgoing_message = OutgoingMessage::Notification(OutgoingNotification {
+ method: event.msg.to_string(),
params,
});
let _ = self.sender.send(outgoing_message).await;
}
-
pub(crate) async fn send_error(&self, id: RequestId, error: JSONRPCErrorError) {
let outgoing_message = OutgoingMessage::Error(OutgoingError { id, error });
let _ = self.sender.send(outgoing_message).await;
diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs
index b27a96eb89..528a40152f 100644
--- a/codex-rs/mcp-server/tests/common/mcp_process.rs
+++ b/codex-rs/mcp-server/tests/common/mcp_process.rs
@@ -270,27 +270,49 @@ impl McpProcess {
pub async fn read_stream_until_configured_response_message(
&mut self,
) -> anyhow::Result<String> {
+ let mut sid_old: Option<String> = None;
+ let mut sid_new: Option<String> = None;
loop {
let message = self.read_jsonrpc_message().await?;
eprint!("message: {message:?}");
match message {
JSONRPCMessage::Notification(notification) => {
- if notification.method == "codex/event" {
- if let Some(params) = notification.params {
+ if let Some(params) = notification.params {
+ // Back-compat schema: method == "codex/event" and msg.type == "session_configured"
+ if notification.method == "codex/event" {
if let Some(msg) = params.get("msg") {
- if let Some(msg_type) = msg.get("type") {
- if msg_type == "session_configured" {
- if let Some(session_id) = msg.get("session_id") {
- return Ok(session_id
- .to_string()
- .trim_matches('"')
- .to_string());
- }
+ if msg.get("type").and_then(|v| v.as_str())
+ == Some("session_configured")
+ {
+ if let Some(session_id) =
+ msg.get("session_id").and_then(|v| v.as_str())
+ {
+ sid_old = Some(session_id.to_string());
}
}
}
}
+ // New schema: method is the Display of EventMsg::SessionConfigured => "SessionConfigured"
+ if notification.method == "sessionconfigured" {
+ if let Some(msg) = params.get("msg") {
+ if let Some(session_id) =
+ msg.get("session_id").and_then(|v| v.as_str())
+ {
+ sid_new = Some(session_id.to_string());
+ }
+ }
+ }
+ }
+
+ if sid_old.is_some() && sid_new.is_some() {
+ // Both seen, they must match
+ assert_eq!(
+ sid_old.as_ref().unwrap(),
+ sid_new.as_ref().unwrap(),
+ "session_id mismatch between old and new schema"
+ );
+ return Ok(sid_old.unwrap());
}
}
JSONRPCMessage::Request(_) => {
```
## Review Comments
### codex-rs/core/src/protocol.rs
- Created: 2025-07-26 03:09:43 UTC | Link: https://github.com/openai/codex/pull/1684#discussion_r2232408024
```diff
@@ -338,6 +338,34 @@ pub enum EventMsg {
ShutdownComplete,
}
+impl fmt::Display for EventMsg {
```
> Please use strum macros instead and use lowercase?
- Created: 2025-07-27 03:03:59 UTC | Link: https://github.com/openai/codex/pull/1684#discussion_r2233616677
```diff
@@ -278,8 +278,9 @@ pub struct Event {
}
/// Response event from the agent
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(Debug, Clone, Deserialize, Serialize, Display)]
#[serde(tag = "type", rename_all = "snake_case")]
+#[strum(serialize_all = "lowercase")]
```
> I misspoke, I think this should not be plain `lowercase`, but likely one of `snake_case` or `camelCase`, whatever we decide. While Rust prefers `snake_case`, MCP prefers `camelCase`, so perhaps we should adopt that here since the primary serialization use case is MCP?
### codex-rs/mcp-server/src/outgoing_message.rs
- Created: 2025-07-27 03:04:47 UTC | Link: https://github.com/openai/codex/pull/1684#discussion_r2233616831
```diff
@@ -83,11 +83,26 @@ impl OutgoingMessageSender {
let params = Some(serde_json::to_value(event).expect("Event must serialize"));
let outgoing_message = OutgoingMessage::Notification(OutgoingNotification {
method: "codex/event".to_string(),
+ params: params.clone(),
+ });
+ let _ = self.sender.send(outgoing_message).await;
+
+ self.send_event_as_notification_new_schema(event, params)
+ .await;
+ }
+ // should be backwards compatible.
```
> I believe this is a net-new set of notifications. The old ones all have `codex/event` as the method whereas the new ones use the specific notification type. It should be fine to disambiguate.
- Created: 2025-07-27 03:05:20 UTC | Link: https://github.com/openai/codex/pull/1684#discussion_r2233617072
```diff
@@ -83,11 +83,26 @@ impl OutgoingMessageSender {
let params = Some(serde_json::to_value(event).expect("Event must serialize"));
let outgoing_message = OutgoingMessage::Notification(OutgoingNotification {
method: "codex/event".to_string(),
+ params: params.clone(),
+ });
+ let _ = self.sender.send(outgoing_message).await;
+
+ self.send_event_as_notification_new_schema(event, params)
+ .await;
+ }
+ // should be backwards compatible.
+ // it will replace send_event_as_notification eventually.
+ async fn send_event_as_notification_new_schema(
+ &self,
+ event: &Event,
+ params: Option<serde_json::Value>,
```
> Though we also plan to change the `params` in the new schema, but that is not obvious from this general datatype...
- Created: 2025-07-27 03:05:31 UTC | Link: https://github.com/openai/codex/pull/1684#discussion_r2233617124
```diff
@@ -83,11 +83,26 @@ impl OutgoingMessageSender {
let params = Some(serde_json::to_value(event).expect("Event must serialize"));
let outgoing_message = OutgoingMessage::Notification(OutgoingNotification {
method: "codex/event".to_string(),
+ params: params.clone(),
+ });
+ let _ = self.sender.send(outgoing_message).await;
+
+ self.send_event_as_notification_new_schema(event, params)
+ .await;
+ }
+ // should be backwards compatible.
+ // it will replace send_event_as_notification eventually.
+ async fn send_event_as_notification_new_schema(
+ &self,
+ event: &Event,
+ params: Option<serde_json::Value>,
+ ) {
+ let outgoing_message = OutgoingMessage::Notification(OutgoingNotification {
+ method: event.msg.to_string(),
params,
});
let _ = self.sender.send(outgoing_message).await;
}
-
```
> restore?
### codex-rs/mcp-server/tests/common/mcp_process.rs
- Created: 2025-07-27 01:36:44 UTC | Link: https://github.com/openai/codex/pull/1684#discussion_r2233580571
```diff
@@ -270,27 +270,49 @@ impl McpProcess {
pub async fn read_stream_until_configured_response_message(
&mut self,
) -> anyhow::Result<String> {
+ let mut sid_old: Option<String> = None;
+ let mut sid_new: Option<String> = None;
loop {
let message = self.read_jsonrpc_message().await?;
eprint!("message: {message:?}");
match message {
JSONRPCMessage::Notification(notification) => {
- if notification.method == "codex/event" {
- if let Some(params) = notification.params {
+ if let Some(params) = notification.params {
+ // Back-compat schema: method == "codex/event" and msg.type == "session_configured"
+ if notification.method == "codex/event" {
if let Some(msg) = params.get("msg") {
- if let Some(msg_type) = msg.get("type") {
- if msg_type == "session_configured" {
- if let Some(session_id) = msg.get("session_id") {
- return Ok(session_id
- .to_string()
- .trim_matches('"')
- .to_string());
- }
+ if msg.get("type").and_then(|v| v.as_str())
+ == Some("session_configured")
+ {
+ if let Some(session_id) =
+ msg.get("session_id").and_then(|v| v.as_str())
+ {
+ sid_old = Some(session_id.to_string());
}
}
}
}
+ // New schema: method is the Display of EventMsg::SessionConfigured => "SessionConfigured"
+ if notification.method == "sessionconfigured" {
```
> It is normal to do this if you use serde or strum macros or something. You can also use these to override the name for individual variants.
- Created: 2025-07-27 03:08:24 UTC | Link: https://github.com/openai/codex/pull/1684#discussion_r2233618044
```diff
@@ -270,27 +270,49 @@ impl McpProcess {
pub async fn read_stream_until_configured_response_message(
&mut self,
) -> anyhow::Result<String> {
+ let mut sid_old: Option<String> = None;
+ let mut sid_new: Option<String> = None;
loop {
let message = self.read_jsonrpc_message().await?;
eprint!("message: {message:?}");
match message {
JSONRPCMessage::Notification(notification) => {
- if notification.method == "codex/event" {
- if let Some(params) = notification.params {
+ if let Some(params) = notification.params {
+ // Back-compat schema: method == "codex/event" and msg.type == "session_configured"
+ if notification.method == "codex/event" {
if let Some(msg) = params.get("msg") {
- if let Some(msg_type) = msg.get("type") {
- if msg_type == "session_configured" {
- if let Some(session_id) = msg.get("session_id") {
- return Ok(session_id
- .to_string()
- .trim_matches('"')
- .to_string());
- }
+ if msg.get("type").and_then(|v| v.as_str())
+ == Some("session_configured")
+ {
+ if let Some(session_id) =
+ msg.get("session_id").and_then(|v| v.as_str())
+ {
+ sid_old = Some(session_id.to_string());
}
}
}
}
+ // New schema: method is the Display of EventMsg::SessionConfigured => "SessionConfigured"
+ if notification.method == "sessionconfigured" {
```
> Note we were also doing this before: it's just that it was on our custom `type` field instead of the `method` field.
>
> In practice, if you have good integration tests, it should catch the fact if someone renames the variant without thinking about the serialization implications. It's that or you end up with a lot of copy paste where ever `rename` line matches the variant name.