feat: support traditional JSON-RPC request/response in MCP server (#2264)
This introduces a new set of request types that our `codex mcp`
supports. Note that these do not conform to MCP tool calls so that
instead of having to send something like this:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 42,
"params": {
"name": "newConversation",
"arguments": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
}
```
we can send something like this:
```json
{
"jsonrpc": "2.0",
"method": "newConversation",
"id": 42,
"params": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
```
Admittedly, this new format is not a valid MCP tool call, but we are OK
with that right now. (That is, not everything we might want to request
of `codex mcp` is something that is appropriate for an autonomous agent
to do.)
To start, this introduces four request types:
- `newConversation`
- `sendUserMessage`
- `addConversationListener`
- `removeConversationListener`
The new `mcp-server/tests/codex_message_processor_flow.rs` shows how
these can be used.
The types are defined on the `CodexRequest` enum, so we introduce a new
`CodexMessageProcessor` that is responsible for dealing with requests
from this enum. The top-level `MessageProcessor` has been updated so
that when `process_request()` is called, it first checks whether the
request conforms to `CodexRequest` and dispatches it to
`CodexMessageProcessor` if so.
Note that I also decided to use `camelCase` for the on-the-wire format,
as that seems to be the convention for MCP.
For the moment, the new protocol is defined in `wire_format.rs` within
the `mcp-server` crate, but in a subsequent PR, I will probably move it
to its own crate to ensure the protocol has minimal dependencies and
that we can codegen a schema from it.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2264).
* #2278
* __->__ #2264
2025-08-13 17:36:29 -07:00
|
|
|
use std::path::Path;
|
|
|
|
|
|
fix: separate `codex mcp` into `codex mcp-server` and `codex app-server` (#4471)
This is a very large PR with some non-backwards-compatible changes.
Historically, `codex mcp` (or `codex mcp serve`) started a JSON-RPC-ish
server that had two overlapping responsibilities:
- Running an MCP server, providing some basic tool calls.
- Running the app server used to power experiences such as the VS Code
extension.
This PR aims to separate these into distinct concepts:
- `codex mcp-server` for the MCP server
- `codex app-server` for the "application server"
Note `codex mcp` still exists because it already has its own subcommands
for MCP management (`list`, `add`, etc.)
The MCP logic continues to live in `codex-rs/mcp-server` whereas the
refactored app server logic is in the new `codex-rs/app-server` folder.
Note that most of the existing integration tests in
`codex-rs/mcp-server/tests/suite` were actually for the app server, so
all the tests have been moved with the exception of
`codex-rs/mcp-server/tests/suite/mod.rs`.
Because this is already a large diff, I tried not to change more than I
had to, so `codex-rs/app-server/tests/common/mcp_process.rs` still uses
the name `McpProcess` for now, but I will do some mechanical renamings
to things like `AppServer` in subsequent PRs.
While `mcp-server` and `app-server` share some overlapping functionality
(like reading streams of JSONL and dispatching based on message types)
and some differences (completely different message types), I ended up
doing a bit of copypasta between the two crates, as both have somewhat
similar `message_processor.rs` and `outgoing_message.rs` files for now,
though I expect them to diverge more in the near future.
One material change is that of the initialize handshake for `codex
app-server`, as we no longer use the MCP types for that handshake.
Instead, we update `codex-rs/protocol/src/mcp_protocol.rs` to add an
`Initialize` variant to `ClientRequest`, which takes the `ClientInfo`
object we need to update the `USER_AGENT_SUFFIX` in
`codex-rs/app-server/src/message_processor.rs`.
One other material change is in
`codex-rs/app-server/src/codex_message_processor.rs` where I eliminated
a use of the `send_event_as_notification()` method I am generally trying
to deprecate (because it blindly maps an `EventMsg` into a
`JSONNotification`) in favor of `send_server_notification()`, which
takes a `ServerNotification`, as that is intended to be a custom enum of
all notification types supported by the app server. So to make this
update, I had to introduce a new variant of `ServerNotification`,
`SessionConfigured`, which is a non-backwards compatible change with the
old `codex mcp`, and clients will have to be updated after the next
release that contains this PR. Note that
`codex-rs/app-server/tests/suite/list_resume.rs` also had to be update
to reflect this change.
I introduced `codex-rs/utils/json-to-toml/src/lib.rs` as a small utility
crate to avoid some of the copying between `mcp-server` and
`app-server`.
2025-09-30 00:06:18 -07:00
|
|
|
use app_test_support::McpProcess;
|
|
|
|
|
use app_test_support::create_final_assistant_message_sse_response;
|
|
|
|
|
use app_test_support::create_mock_chat_completions_server;
|
|
|
|
|
use app_test_support::create_shell_sse_response;
|
|
|
|
|
use app_test_support::to_response;
|
fix: remove mcp-types from app server protocol (#4537)
We continue the separation between `codex app-server` and `codex
mcp-server`.
In particular, we introduce a new crate, `codex-app-server-protocol`,
and migrate `codex-rs/protocol/src/mcp_protocol.rs` into it, renaming it
`codex-rs/app-server-protocol/src/protocol.rs`.
Because `ConversationId` was defined in `mcp_protocol.rs`, we move it
into its own file, `codex-rs/protocol/src/conversation_id.rs`, and
because it is referenced in a ton of places, we have to touch a lot of
files as part of this PR.
We also decide to get away from proper JSON-RPC 2.0 semantics, so we
also introduce `codex-rs/app-server-protocol/src/jsonrpc_lite.rs`, which
is basically the same `JSONRPCMessage` type defined in `mcp-types`
except with all of the `"jsonrpc": "2.0"` removed.
Getting rid of `"jsonrpc": "2.0"` makes our serialization logic
considerably simpler, as we can lean heavier on serde to serialize
directly into the wire format that we use now.
2025-09-30 19:16:26 -07:00
|
|
|
use codex_app_server_protocol::AddConversationListenerParams;
|
|
|
|
|
use codex_app_server_protocol::AddConversationSubscriptionResponse;
|
|
|
|
|
use codex_app_server_protocol::ExecCommandApprovalParams;
|
2025-10-01 12:05:12 +01:00
|
|
|
use codex_app_server_protocol::InputItem;
|
fix: remove mcp-types from app server protocol (#4537)
We continue the separation between `codex app-server` and `codex
mcp-server`.
In particular, we introduce a new crate, `codex-app-server-protocol`,
and migrate `codex-rs/protocol/src/mcp_protocol.rs` into it, renaming it
`codex-rs/app-server-protocol/src/protocol.rs`.
Because `ConversationId` was defined in `mcp_protocol.rs`, we move it
into its own file, `codex-rs/protocol/src/conversation_id.rs`, and
because it is referenced in a ton of places, we have to touch a lot of
files as part of this PR.
We also decide to get away from proper JSON-RPC 2.0 semantics, so we
also introduce `codex-rs/app-server-protocol/src/jsonrpc_lite.rs`, which
is basically the same `JSONRPCMessage` type defined in `mcp-types`
except with all of the `"jsonrpc": "2.0"` removed.
Getting rid of `"jsonrpc": "2.0"` makes our serialization logic
considerably simpler, as we can lean heavier on serde to serialize
directly into the wire format that we use now.
2025-09-30 19:16:26 -07:00
|
|
|
use codex_app_server_protocol::JSONRPCNotification;
|
|
|
|
|
use codex_app_server_protocol::JSONRPCResponse;
|
|
|
|
|
use codex_app_server_protocol::NewConversationParams;
|
|
|
|
|
use codex_app_server_protocol::NewConversationResponse;
|
|
|
|
|
use codex_app_server_protocol::RemoveConversationListenerParams;
|
|
|
|
|
use codex_app_server_protocol::RemoveConversationSubscriptionResponse;
|
|
|
|
|
use codex_app_server_protocol::RequestId;
|
|
|
|
|
use codex_app_server_protocol::SendUserMessageParams;
|
|
|
|
|
use codex_app_server_protocol::SendUserMessageResponse;
|
|
|
|
|
use codex_app_server_protocol::SendUserTurnParams;
|
|
|
|
|
use codex_app_server_protocol::SendUserTurnResponse;
|
|
|
|
|
use codex_app_server_protocol::ServerRequest;
|
2025-08-15 10:05:58 -07:00
|
|
|
use codex_core::protocol::AskForApproval;
|
|
|
|
|
use codex_core::protocol::SandboxPolicy;
|
2025-08-15 12:44:40 -07:00
|
|
|
use codex_core::protocol_config_types::ReasoningEffort;
|
|
|
|
|
use codex_core::protocol_config_types::ReasoningSummary;
|
feat: support traditional JSON-RPC request/response in MCP server (#2264)
This introduces a new set of request types that our `codex mcp`
supports. Note that these do not conform to MCP tool calls so that
instead of having to send something like this:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 42,
"params": {
"name": "newConversation",
"arguments": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
}
```
we can send something like this:
```json
{
"jsonrpc": "2.0",
"method": "newConversation",
"id": 42,
"params": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
```
Admittedly, this new format is not a valid MCP tool call, but we are OK
with that right now. (That is, not everything we might want to request
of `codex mcp` is something that is appropriate for an autonomous agent
to do.)
To start, this introduces four request types:
- `newConversation`
- `sendUserMessage`
- `addConversationListener`
- `removeConversationListener`
The new `mcp-server/tests/codex_message_processor_flow.rs` shows how
these can be used.
The types are defined on the `CodexRequest` enum, so we introduce a new
`CodexMessageProcessor` that is responsible for dealing with requests
from this enum. The top-level `MessageProcessor` has been updated so
that when `process_request()` is called, it first checks whether the
request conforms to `CodexRequest` and dispatches it to
`CodexMessageProcessor` if so.
Note that I also decided to use `camelCase` for the on-the-wire format,
as that seems to be the convention for MCP.
For the moment, the new protocol is defined in `wire_format.rs` within
the `mcp-server` crate, but in a subsequent PR, I will probably move it
to its own crate to ensure the protocol has minimal dependencies and
that we can codegen a schema from it.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2264).
* #2278
* __->__ #2264
2025-08-13 17:36:29 -07:00
|
|
|
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
2025-10-01 12:05:12 +01:00
|
|
|
use codex_protocol::config_types::SandboxMode;
|
|
|
|
|
use codex_protocol::protocol::Event;
|
|
|
|
|
use codex_protocol::protocol::EventMsg;
|
|
|
|
|
use codex_protocol::protocol::InputMessageKind;
|
feat: support traditional JSON-RPC request/response in MCP server (#2264)
This introduces a new set of request types that our `codex mcp`
supports. Note that these do not conform to MCP tool calls so that
instead of having to send something like this:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 42,
"params": {
"name": "newConversation",
"arguments": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
}
```
we can send something like this:
```json
{
"jsonrpc": "2.0",
"method": "newConversation",
"id": 42,
"params": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
```
Admittedly, this new format is not a valid MCP tool call, but we are OK
with that right now. (That is, not everything we might want to request
of `codex mcp` is something that is appropriate for an autonomous agent
to do.)
To start, this introduces four request types:
- `newConversation`
- `sendUserMessage`
- `addConversationListener`
- `removeConversationListener`
The new `mcp-server/tests/codex_message_processor_flow.rs` shows how
these can be used.
The types are defined on the `CodexRequest` enum, so we introduce a new
`CodexMessageProcessor` that is responsible for dealing with requests
from this enum. The top-level `MessageProcessor` has been updated so
that when `process_request()` is called, it first checks whether the
request conforms to `CodexRequest` and dispatches it to
`CodexMessageProcessor` if so.
Note that I also decided to use `camelCase` for the on-the-wire format,
as that seems to be the convention for MCP.
For the moment, the new protocol is defined in `wire_format.rs` within
the `mcp-server` crate, but in a subsequent PR, I will probably move it
to its own crate to ensure the protocol has minimal dependencies and
that we can codegen a schema from it.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2264).
* #2278
* __->__ #2264
2025-08-13 17:36:29 -07:00
|
|
|
use pretty_assertions::assert_eq;
|
|
|
|
|
use std::env;
|
|
|
|
|
use tempfile::TempDir;
|
|
|
|
|
use tokio::time::timeout;
|
|
|
|
|
|
|
|
|
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
|
|
|
async fn test_codex_jsonrpc_conversation_flow() {
|
|
|
|
|
if env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
|
|
|
|
println!(
|
|
|
|
|
"Skipping test because it cannot execute when network is disabled in a Codex sandbox."
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tmp = TempDir::new().expect("tmp dir");
|
|
|
|
|
// Temporary Codex home with config pointing at the mock server.
|
|
|
|
|
let codex_home = tmp.path().join("codex_home");
|
|
|
|
|
std::fs::create_dir(&codex_home).expect("create codex home dir");
|
|
|
|
|
let working_directory = tmp.path().join("workdir");
|
|
|
|
|
std::fs::create_dir(&working_directory).expect("create working directory");
|
|
|
|
|
|
|
|
|
|
// Create a mock model server that immediately ends each turn.
|
|
|
|
|
// Two turns are expected: initial session configure + one user message.
|
|
|
|
|
let responses = vec![
|
|
|
|
|
create_shell_sse_response(
|
|
|
|
|
vec!["ls".to_string()],
|
|
|
|
|
Some(&working_directory),
|
|
|
|
|
Some(5000),
|
|
|
|
|
"call1234",
|
|
|
|
|
)
|
|
|
|
|
.expect("create shell sse response"),
|
|
|
|
|
create_final_assistant_message_sse_response("Enjoy your new git repo!")
|
|
|
|
|
.expect("create final assistant message"),
|
|
|
|
|
];
|
|
|
|
|
let server = create_mock_chat_completions_server(responses).await;
|
|
|
|
|
create_config_toml(&codex_home, &server.uri()).expect("write config");
|
|
|
|
|
|
|
|
|
|
// Start MCP server and initialize.
|
|
|
|
|
let mut mcp = McpProcess::new(&codex_home).await.expect("spawn mcp");
|
|
|
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize())
|
|
|
|
|
.await
|
|
|
|
|
.expect("init timeout")
|
|
|
|
|
.expect("init error");
|
|
|
|
|
|
|
|
|
|
// 1) newConversation
|
|
|
|
|
let new_conv_id = mcp
|
|
|
|
|
.send_new_conversation_request(NewConversationParams {
|
|
|
|
|
cwd: Some(working_directory.to_string_lossy().into_owned()),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send newConversation");
|
|
|
|
|
let new_conv_resp: JSONRPCResponse = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("newConversation timeout")
|
|
|
|
|
.expect("newConversation resp");
|
|
|
|
|
let new_conv_resp = to_response::<NewConversationResponse>(new_conv_resp)
|
|
|
|
|
.expect("deserialize newConversation response");
|
|
|
|
|
let NewConversationResponse {
|
|
|
|
|
conversation_id,
|
|
|
|
|
model,
|
2025-09-11 21:04:40 -07:00
|
|
|
reasoning_effort: _,
|
2025-09-09 00:11:48 -07:00
|
|
|
rollout_path: _,
|
feat: support traditional JSON-RPC request/response in MCP server (#2264)
This introduces a new set of request types that our `codex mcp`
supports. Note that these do not conform to MCP tool calls so that
instead of having to send something like this:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 42,
"params": {
"name": "newConversation",
"arguments": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
}
```
we can send something like this:
```json
{
"jsonrpc": "2.0",
"method": "newConversation",
"id": 42,
"params": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
```
Admittedly, this new format is not a valid MCP tool call, but we are OK
with that right now. (That is, not everything we might want to request
of `codex mcp` is something that is appropriate for an autonomous agent
to do.)
To start, this introduces four request types:
- `newConversation`
- `sendUserMessage`
- `addConversationListener`
- `removeConversationListener`
The new `mcp-server/tests/codex_message_processor_flow.rs` shows how
these can be used.
The types are defined on the `CodexRequest` enum, so we introduce a new
`CodexMessageProcessor` that is responsible for dealing with requests
from this enum. The top-level `MessageProcessor` has been updated so
that when `process_request()` is called, it first checks whether the
request conforms to `CodexRequest` and dispatches it to
`CodexMessageProcessor` if so.
Note that I also decided to use `camelCase` for the on-the-wire format,
as that seems to be the convention for MCP.
For the moment, the new protocol is defined in `wire_format.rs` within
the `mcp-server` crate, but in a subsequent PR, I will probably move it
to its own crate to ensure the protocol has minimal dependencies and
that we can codegen a schema from it.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2264).
* #2278
* __->__ #2264
2025-08-13 17:36:29 -07:00
|
|
|
} = new_conv_resp;
|
|
|
|
|
assert_eq!(model, "mock-model");
|
|
|
|
|
|
|
|
|
|
// 2) addConversationListener
|
|
|
|
|
let add_listener_id = mcp
|
|
|
|
|
.send_add_conversation_listener_request(AddConversationListenerParams { conversation_id })
|
|
|
|
|
.await
|
|
|
|
|
.expect("send addConversationListener");
|
|
|
|
|
let add_listener_resp: JSONRPCResponse = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(add_listener_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("addConversationListener timeout")
|
|
|
|
|
.expect("addConversationListener resp");
|
|
|
|
|
let AddConversationSubscriptionResponse { subscription_id } =
|
|
|
|
|
to_response::<AddConversationSubscriptionResponse>(add_listener_resp)
|
|
|
|
|
.expect("deserialize addConversationListener response");
|
|
|
|
|
|
|
|
|
|
// 3) sendUserMessage (should trigger notifications; we only validate an OK response)
|
|
|
|
|
let send_user_id = mcp
|
|
|
|
|
.send_send_user_message_request(SendUserMessageParams {
|
|
|
|
|
conversation_id,
|
fix: remove mcp-types from app server protocol (#4537)
We continue the separation between `codex app-server` and `codex
mcp-server`.
In particular, we introduce a new crate, `codex-app-server-protocol`,
and migrate `codex-rs/protocol/src/mcp_protocol.rs` into it, renaming it
`codex-rs/app-server-protocol/src/protocol.rs`.
Because `ConversationId` was defined in `mcp_protocol.rs`, we move it
into its own file, `codex-rs/protocol/src/conversation_id.rs`, and
because it is referenced in a ton of places, we have to touch a lot of
files as part of this PR.
We also decide to get away from proper JSON-RPC 2.0 semantics, so we
also introduce `codex-rs/app-server-protocol/src/jsonrpc_lite.rs`, which
is basically the same `JSONRPCMessage` type defined in `mcp-types`
except with all of the `"jsonrpc": "2.0"` removed.
Getting rid of `"jsonrpc": "2.0"` makes our serialization logic
considerably simpler, as we can lean heavier on serde to serialize
directly into the wire format that we use now.
2025-09-30 19:16:26 -07:00
|
|
|
items: vec![codex_app_server_protocol::InputItem::Text {
|
feat: support traditional JSON-RPC request/response in MCP server (#2264)
This introduces a new set of request types that our `codex mcp`
supports. Note that these do not conform to MCP tool calls so that
instead of having to send something like this:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 42,
"params": {
"name": "newConversation",
"arguments": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
}
```
we can send something like this:
```json
{
"jsonrpc": "2.0",
"method": "newConversation",
"id": 42,
"params": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
```
Admittedly, this new format is not a valid MCP tool call, but we are OK
with that right now. (That is, not everything we might want to request
of `codex mcp` is something that is appropriate for an autonomous agent
to do.)
To start, this introduces four request types:
- `newConversation`
- `sendUserMessage`
- `addConversationListener`
- `removeConversationListener`
The new `mcp-server/tests/codex_message_processor_flow.rs` shows how
these can be used.
The types are defined on the `CodexRequest` enum, so we introduce a new
`CodexMessageProcessor` that is responsible for dealing with requests
from this enum. The top-level `MessageProcessor` has been updated so
that when `process_request()` is called, it first checks whether the
request conforms to `CodexRequest` and dispatches it to
`CodexMessageProcessor` if so.
Note that I also decided to use `camelCase` for the on-the-wire format,
as that seems to be the convention for MCP.
For the moment, the new protocol is defined in `wire_format.rs` within
the `mcp-server` crate, but in a subsequent PR, I will probably move it
to its own crate to ensure the protocol has minimal dependencies and
that we can codegen a schema from it.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2264).
* #2278
* __->__ #2264
2025-08-13 17:36:29 -07:00
|
|
|
text: "text".to_string(),
|
|
|
|
|
}],
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send sendUserMessage");
|
|
|
|
|
let send_user_resp: JSONRPCResponse = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(send_user_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("sendUserMessage timeout")
|
|
|
|
|
.expect("sendUserMessage resp");
|
|
|
|
|
let SendUserMessageResponse {} = to_response::<SendUserMessageResponse>(send_user_resp)
|
|
|
|
|
.expect("deserialize sendUserMessage response");
|
|
|
|
|
|
2025-08-13 17:54:12 -07:00
|
|
|
// Verify the task_finished notification is received.
|
|
|
|
|
// Note this also ensures that the final request to the server was made.
|
|
|
|
|
let task_finished_notification: JSONRPCNotification = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_notification_message("codex/event/task_complete"),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("task_finished_notification timeout")
|
|
|
|
|
.expect("task_finished_notification resp");
|
|
|
|
|
let serde_json::Value::Object(map) = task_finished_notification
|
|
|
|
|
.params
|
|
|
|
|
.expect("notification should have params")
|
|
|
|
|
else {
|
|
|
|
|
panic!("task_finished_notification should have params");
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
map.get("conversationId")
|
|
|
|
|
.expect("should have conversationId"),
|
|
|
|
|
&serde_json::Value::String(conversation_id.to_string())
|
|
|
|
|
);
|
feat: support traditional JSON-RPC request/response in MCP server (#2264)
This introduces a new set of request types that our `codex mcp`
supports. Note that these do not conform to MCP tool calls so that
instead of having to send something like this:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 42,
"params": {
"name": "newConversation",
"arguments": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
}
```
we can send something like this:
```json
{
"jsonrpc": "2.0",
"method": "newConversation",
"id": 42,
"params": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
```
Admittedly, this new format is not a valid MCP tool call, but we are OK
with that right now. (That is, not everything we might want to request
of `codex mcp` is something that is appropriate for an autonomous agent
to do.)
To start, this introduces four request types:
- `newConversation`
- `sendUserMessage`
- `addConversationListener`
- `removeConversationListener`
The new `mcp-server/tests/codex_message_processor_flow.rs` shows how
these can be used.
The types are defined on the `CodexRequest` enum, so we introduce a new
`CodexMessageProcessor` that is responsible for dealing with requests
from this enum. The top-level `MessageProcessor` has been updated so
that when `process_request()` is called, it first checks whether the
request conforms to `CodexRequest` and dispatches it to
`CodexMessageProcessor` if so.
Note that I also decided to use `camelCase` for the on-the-wire format,
as that seems to be the convention for MCP.
For the moment, the new protocol is defined in `wire_format.rs` within
the `mcp-server` crate, but in a subsequent PR, I will probably move it
to its own crate to ensure the protocol has minimal dependencies and
that we can codegen a schema from it.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2264).
* #2278
* __->__ #2264
2025-08-13 17:36:29 -07:00
|
|
|
|
|
|
|
|
// 4) removeConversationListener
|
|
|
|
|
let remove_listener_id = mcp
|
|
|
|
|
.send_remove_conversation_listener_request(RemoveConversationListenerParams {
|
|
|
|
|
subscription_id,
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send removeConversationListener");
|
|
|
|
|
let remove_listener_resp: JSONRPCResponse = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(remove_listener_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("removeConversationListener timeout")
|
|
|
|
|
.expect("removeConversationListener resp");
|
|
|
|
|
let RemoveConversationSubscriptionResponse {} =
|
|
|
|
|
to_response(remove_listener_resp).expect("deserialize removeConversationListener response");
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-15 10:05:58 -07:00
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
|
|
|
async fn test_send_user_turn_changes_approval_policy_behavior() {
|
|
|
|
|
if env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
|
|
|
|
println!(
|
|
|
|
|
"Skipping test because it cannot execute when network is disabled in a Codex sandbox."
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tmp = TempDir::new().expect("tmp dir");
|
|
|
|
|
let codex_home = tmp.path().join("codex_home");
|
|
|
|
|
std::fs::create_dir(&codex_home).expect("create codex home dir");
|
|
|
|
|
let working_directory = tmp.path().join("workdir");
|
|
|
|
|
std::fs::create_dir(&working_directory).expect("create working directory");
|
|
|
|
|
|
|
|
|
|
// Mock server will request a python shell call for the first and second turn, then finish.
|
|
|
|
|
let responses = vec![
|
|
|
|
|
create_shell_sse_response(
|
|
|
|
|
vec![
|
|
|
|
|
"python3".to_string(),
|
|
|
|
|
"-c".to_string(),
|
|
|
|
|
"print(42)".to_string(),
|
|
|
|
|
],
|
|
|
|
|
Some(&working_directory),
|
|
|
|
|
Some(5000),
|
|
|
|
|
"call1",
|
|
|
|
|
)
|
|
|
|
|
.expect("create first shell sse response"),
|
|
|
|
|
create_final_assistant_message_sse_response("done 1")
|
|
|
|
|
.expect("create final assistant message 1"),
|
|
|
|
|
create_shell_sse_response(
|
|
|
|
|
vec![
|
|
|
|
|
"python3".to_string(),
|
|
|
|
|
"-c".to_string(),
|
|
|
|
|
"print(42)".to_string(),
|
|
|
|
|
],
|
|
|
|
|
Some(&working_directory),
|
|
|
|
|
Some(5000),
|
|
|
|
|
"call2",
|
|
|
|
|
)
|
|
|
|
|
.expect("create second shell sse response"),
|
|
|
|
|
create_final_assistant_message_sse_response("done 2")
|
|
|
|
|
.expect("create final assistant message 2"),
|
|
|
|
|
];
|
|
|
|
|
let server = create_mock_chat_completions_server(responses).await;
|
|
|
|
|
create_config_toml(&codex_home, &server.uri()).expect("write config");
|
|
|
|
|
|
|
|
|
|
// Start MCP server and initialize.
|
|
|
|
|
let mut mcp = McpProcess::new(&codex_home).await.expect("spawn mcp");
|
|
|
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize())
|
|
|
|
|
.await
|
|
|
|
|
.expect("init timeout")
|
|
|
|
|
.expect("init error");
|
|
|
|
|
|
|
|
|
|
// 1) Start conversation with approval_policy=untrusted
|
|
|
|
|
let new_conv_id = mcp
|
|
|
|
|
.send_new_conversation_request(NewConversationParams {
|
|
|
|
|
cwd: Some(working_directory.to_string_lossy().into_owned()),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send newConversation");
|
|
|
|
|
let new_conv_resp: JSONRPCResponse = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("newConversation timeout")
|
|
|
|
|
.expect("newConversation resp");
|
|
|
|
|
let NewConversationResponse {
|
|
|
|
|
conversation_id, ..
|
|
|
|
|
} = to_response::<NewConversationResponse>(new_conv_resp)
|
|
|
|
|
.expect("deserialize newConversation response");
|
|
|
|
|
|
|
|
|
|
// 2) addConversationListener
|
|
|
|
|
let add_listener_id = mcp
|
|
|
|
|
.send_add_conversation_listener_request(AddConversationListenerParams { conversation_id })
|
|
|
|
|
.await
|
|
|
|
|
.expect("send addConversationListener");
|
|
|
|
|
let _: AddConversationSubscriptionResponse =
|
|
|
|
|
to_response::<AddConversationSubscriptionResponse>(
|
|
|
|
|
timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(add_listener_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("addConversationListener timeout")
|
|
|
|
|
.expect("addConversationListener resp"),
|
|
|
|
|
)
|
|
|
|
|
.expect("deserialize addConversationListener response");
|
|
|
|
|
|
|
|
|
|
// 3) sendUserMessage triggers a shell call; approval policy is Untrusted so we should get an elicitation
|
|
|
|
|
let send_user_id = mcp
|
|
|
|
|
.send_send_user_message_request(SendUserMessageParams {
|
|
|
|
|
conversation_id,
|
fix: remove mcp-types from app server protocol (#4537)
We continue the separation between `codex app-server` and `codex
mcp-server`.
In particular, we introduce a new crate, `codex-app-server-protocol`,
and migrate `codex-rs/protocol/src/mcp_protocol.rs` into it, renaming it
`codex-rs/app-server-protocol/src/protocol.rs`.
Because `ConversationId` was defined in `mcp_protocol.rs`, we move it
into its own file, `codex-rs/protocol/src/conversation_id.rs`, and
because it is referenced in a ton of places, we have to touch a lot of
files as part of this PR.
We also decide to get away from proper JSON-RPC 2.0 semantics, so we
also introduce `codex-rs/app-server-protocol/src/jsonrpc_lite.rs`, which
is basically the same `JSONRPCMessage` type defined in `mcp-types`
except with all of the `"jsonrpc": "2.0"` removed.
Getting rid of `"jsonrpc": "2.0"` makes our serialization logic
considerably simpler, as we can lean heavier on serde to serialize
directly into the wire format that we use now.
2025-09-30 19:16:26 -07:00
|
|
|
items: vec![codex_app_server_protocol::InputItem::Text {
|
2025-08-15 10:05:58 -07:00
|
|
|
text: "run python".to_string(),
|
|
|
|
|
}],
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send sendUserMessage");
|
|
|
|
|
let _send_user_resp: SendUserMessageResponse = to_response::<SendUserMessageResponse>(
|
|
|
|
|
timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(send_user_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("sendUserMessage timeout")
|
|
|
|
|
.expect("sendUserMessage resp"),
|
|
|
|
|
)
|
|
|
|
|
.expect("deserialize sendUserMessage response");
|
|
|
|
|
|
|
|
|
|
// Expect an ExecCommandApproval request (elicitation)
|
|
|
|
|
let request = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_request_message(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("waiting for exec approval request timeout")
|
|
|
|
|
.expect("exec approval request");
|
2025-09-30 18:06:05 -07:00
|
|
|
let ServerRequest::ExecCommandApproval { request_id, params } = request else {
|
|
|
|
|
panic!("expected ExecCommandApproval request, got: {request:?}");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ExecCommandApprovalParams {
|
|
|
|
|
conversation_id,
|
|
|
|
|
call_id: "call1".to_string(),
|
|
|
|
|
command: vec![
|
|
|
|
|
"python3".to_string(),
|
|
|
|
|
"-c".to_string(),
|
|
|
|
|
"print(42)".to_string(),
|
|
|
|
|
],
|
|
|
|
|
cwd: working_directory.clone(),
|
|
|
|
|
reason: None,
|
|
|
|
|
},
|
|
|
|
|
params
|
|
|
|
|
);
|
2025-08-15 10:05:58 -07:00
|
|
|
|
|
|
|
|
// Approve so the first turn can complete
|
|
|
|
|
mcp.send_response(
|
2025-09-30 18:06:05 -07:00
|
|
|
request_id,
|
2025-08-15 10:05:58 -07:00
|
|
|
serde_json::json!({ "decision": codex_core::protocol::ReviewDecision::Approved }),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("send approval response");
|
|
|
|
|
|
|
|
|
|
// Wait for first TaskComplete
|
|
|
|
|
let _ = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_notification_message("codex/event/task_complete"),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("task_complete 1 timeout")
|
|
|
|
|
.expect("task_complete 1 notification");
|
|
|
|
|
|
|
|
|
|
// 4) sendUserTurn with approval_policy=never should run without elicitation
|
|
|
|
|
let send_turn_id = mcp
|
|
|
|
|
.send_send_user_turn_request(SendUserTurnParams {
|
|
|
|
|
conversation_id,
|
fix: remove mcp-types from app server protocol (#4537)
We continue the separation between `codex app-server` and `codex
mcp-server`.
In particular, we introduce a new crate, `codex-app-server-protocol`,
and migrate `codex-rs/protocol/src/mcp_protocol.rs` into it, renaming it
`codex-rs/app-server-protocol/src/protocol.rs`.
Because `ConversationId` was defined in `mcp_protocol.rs`, we move it
into its own file, `codex-rs/protocol/src/conversation_id.rs`, and
because it is referenced in a ton of places, we have to touch a lot of
files as part of this PR.
We also decide to get away from proper JSON-RPC 2.0 semantics, so we
also introduce `codex-rs/app-server-protocol/src/jsonrpc_lite.rs`, which
is basically the same `JSONRPCMessage` type defined in `mcp-types`
except with all of the `"jsonrpc": "2.0"` removed.
Getting rid of `"jsonrpc": "2.0"` makes our serialization logic
considerably simpler, as we can lean heavier on serde to serialize
directly into the wire format that we use now.
2025-09-30 19:16:26 -07:00
|
|
|
items: vec![codex_app_server_protocol::InputItem::Text {
|
2025-08-15 10:05:58 -07:00
|
|
|
text: "run python again".to_string(),
|
|
|
|
|
}],
|
|
|
|
|
cwd: working_directory.clone(),
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
|
|
|
|
model: "mock-model".to_string(),
|
2025-09-12 12:06:33 -07:00
|
|
|
effort: Some(ReasoningEffort::Medium),
|
2025-08-15 10:05:58 -07:00
|
|
|
summary: ReasoningSummary::Auto,
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send sendUserTurn");
|
|
|
|
|
// Acknowledge sendUserTurn
|
|
|
|
|
let _send_turn_resp: SendUserTurnResponse = to_response::<SendUserTurnResponse>(
|
|
|
|
|
timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(send_turn_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("sendUserTurn timeout")
|
|
|
|
|
.expect("sendUserTurn resp"),
|
|
|
|
|
)
|
|
|
|
|
.expect("deserialize sendUserTurn response");
|
|
|
|
|
|
|
|
|
|
// Ensure we do NOT receive an ExecCommandApproval request before the task completes.
|
|
|
|
|
// If any Request is seen while waiting for task_complete, the helper will error and the test fails.
|
|
|
|
|
let _ = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_notification_message("codex/event/task_complete"),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("task_complete 2 timeout")
|
|
|
|
|
.expect("task_complete 2 notification");
|
|
|
|
|
}
|
|
|
|
|
|
feat: support traditional JSON-RPC request/response in MCP server (#2264)
This introduces a new set of request types that our `codex mcp`
supports. Note that these do not conform to MCP tool calls so that
instead of having to send something like this:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 42,
"params": {
"name": "newConversation",
"arguments": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
}
```
we can send something like this:
```json
{
"jsonrpc": "2.0",
"method": "newConversation",
"id": 42,
"params": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
```
Admittedly, this new format is not a valid MCP tool call, but we are OK
with that right now. (That is, not everything we might want to request
of `codex mcp` is something that is appropriate for an autonomous agent
to do.)
To start, this introduces four request types:
- `newConversation`
- `sendUserMessage`
- `addConversationListener`
- `removeConversationListener`
The new `mcp-server/tests/codex_message_processor_flow.rs` shows how
these can be used.
The types are defined on the `CodexRequest` enum, so we introduce a new
`CodexMessageProcessor` that is responsible for dealing with requests
from this enum. The top-level `MessageProcessor` has been updated so
that when `process_request()` is called, it first checks whether the
request conforms to `CodexRequest` and dispatches it to
`CodexMessageProcessor` if so.
Note that I also decided to use `camelCase` for the on-the-wire format,
as that seems to be the convention for MCP.
For the moment, the new protocol is defined in `wire_format.rs` within
the `mcp-server` crate, but in a subsequent PR, I will probably move it
to its own crate to ensure the protocol has minimal dependencies and
that we can codegen a schema from it.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2264).
* #2278
* __->__ #2264
2025-08-13 17:36:29 -07:00
|
|
|
// Helper: minimal config.toml pointing at mock provider.
|
2025-10-01 12:05:12 +01:00
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
|
|
|
async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() {
|
|
|
|
|
if env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
|
|
|
|
println!(
|
|
|
|
|
"Skipping test because it cannot execute when network is disabled in a Codex sandbox."
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tmp = TempDir::new().expect("tmp dir");
|
|
|
|
|
let codex_home = tmp.path().join("codex_home");
|
|
|
|
|
std::fs::create_dir(&codex_home).expect("create codex home dir");
|
|
|
|
|
let workspace_root = tmp.path().join("workspace");
|
|
|
|
|
std::fs::create_dir(&workspace_root).expect("create workspace root");
|
|
|
|
|
let first_cwd = workspace_root.join("turn1");
|
|
|
|
|
let second_cwd = workspace_root.join("turn2");
|
|
|
|
|
std::fs::create_dir(&first_cwd).expect("create first cwd");
|
|
|
|
|
std::fs::create_dir(&second_cwd).expect("create second cwd");
|
|
|
|
|
|
|
|
|
|
let responses = vec![
|
|
|
|
|
create_shell_sse_response(
|
|
|
|
|
vec![
|
|
|
|
|
"bash".to_string(),
|
|
|
|
|
"-lc".to_string(),
|
|
|
|
|
"echo first turn".to_string(),
|
|
|
|
|
],
|
|
|
|
|
None,
|
|
|
|
|
Some(5000),
|
|
|
|
|
"call-first",
|
|
|
|
|
)
|
|
|
|
|
.expect("create first shell response"),
|
|
|
|
|
create_final_assistant_message_sse_response("done first")
|
|
|
|
|
.expect("create first final assistant message"),
|
|
|
|
|
create_shell_sse_response(
|
|
|
|
|
vec![
|
|
|
|
|
"bash".to_string(),
|
|
|
|
|
"-lc".to_string(),
|
|
|
|
|
"echo second turn".to_string(),
|
|
|
|
|
],
|
|
|
|
|
None,
|
|
|
|
|
Some(5000),
|
|
|
|
|
"call-second",
|
|
|
|
|
)
|
|
|
|
|
.expect("create second shell response"),
|
|
|
|
|
create_final_assistant_message_sse_response("done second")
|
|
|
|
|
.expect("create second final assistant message"),
|
|
|
|
|
];
|
|
|
|
|
let server = create_mock_chat_completions_server(responses).await;
|
|
|
|
|
create_config_toml(&codex_home, &server.uri()).expect("write config");
|
|
|
|
|
|
|
|
|
|
let mut mcp = McpProcess::new(&codex_home)
|
|
|
|
|
.await
|
|
|
|
|
.expect("spawn mcp process");
|
|
|
|
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize())
|
|
|
|
|
.await
|
|
|
|
|
.expect("init timeout")
|
|
|
|
|
.expect("init failed");
|
|
|
|
|
|
|
|
|
|
let new_conv_id = mcp
|
|
|
|
|
.send_new_conversation_request(NewConversationParams {
|
|
|
|
|
cwd: Some(first_cwd.to_string_lossy().into_owned()),
|
|
|
|
|
approval_policy: Some(AskForApproval::Never),
|
|
|
|
|
sandbox: Some(SandboxMode::WorkspaceWrite),
|
|
|
|
|
..Default::default()
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send newConversation");
|
|
|
|
|
let new_conv_resp: JSONRPCResponse = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("newConversation timeout")
|
|
|
|
|
.expect("newConversation resp");
|
|
|
|
|
let NewConversationResponse {
|
|
|
|
|
conversation_id,
|
|
|
|
|
model,
|
|
|
|
|
..
|
|
|
|
|
} = to_response::<NewConversationResponse>(new_conv_resp)
|
|
|
|
|
.expect("deserialize newConversation response");
|
|
|
|
|
|
|
|
|
|
let add_listener_id = mcp
|
|
|
|
|
.send_add_conversation_listener_request(AddConversationListenerParams { conversation_id })
|
|
|
|
|
.await
|
|
|
|
|
.expect("send addConversationListener");
|
|
|
|
|
timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(add_listener_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("addConversationListener timeout")
|
|
|
|
|
.expect("addConversationListener resp");
|
|
|
|
|
|
|
|
|
|
let first_turn_id = mcp
|
|
|
|
|
.send_send_user_turn_request(SendUserTurnParams {
|
|
|
|
|
conversation_id,
|
|
|
|
|
items: vec![InputItem::Text {
|
|
|
|
|
text: "first turn".to_string(),
|
|
|
|
|
}],
|
|
|
|
|
cwd: first_cwd.clone(),
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
sandbox_policy: SandboxPolicy::WorkspaceWrite {
|
|
|
|
|
writable_roots: vec![first_cwd.clone()],
|
|
|
|
|
network_access: false,
|
|
|
|
|
exclude_tmpdir_env_var: false,
|
|
|
|
|
exclude_slash_tmp: false,
|
|
|
|
|
},
|
|
|
|
|
model: model.clone(),
|
|
|
|
|
effort: Some(ReasoningEffort::Medium),
|
|
|
|
|
summary: ReasoningSummary::Auto,
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send first sendUserTurn");
|
|
|
|
|
timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(first_turn_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("sendUserTurn 1 timeout")
|
|
|
|
|
.expect("sendUserTurn 1 resp");
|
|
|
|
|
timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_notification_message("codex/event/task_complete"),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("task_complete 1 timeout")
|
|
|
|
|
.expect("task_complete 1 notification");
|
|
|
|
|
|
|
|
|
|
let second_turn_id = mcp
|
|
|
|
|
.send_send_user_turn_request(SendUserTurnParams {
|
|
|
|
|
conversation_id,
|
|
|
|
|
items: vec![InputItem::Text {
|
|
|
|
|
text: "second turn".to_string(),
|
|
|
|
|
}],
|
|
|
|
|
cwd: second_cwd.clone(),
|
|
|
|
|
approval_policy: AskForApproval::Never,
|
|
|
|
|
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
|
|
|
|
model: model.clone(),
|
|
|
|
|
effort: Some(ReasoningEffort::Medium),
|
|
|
|
|
summary: ReasoningSummary::Auto,
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send second sendUserTurn");
|
|
|
|
|
timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_response_message(RequestId::Integer(second_turn_id)),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("sendUserTurn 2 timeout")
|
|
|
|
|
.expect("sendUserTurn 2 resp");
|
|
|
|
|
|
|
|
|
|
let mut env_message: Option<String> = None;
|
|
|
|
|
let second_cwd_str = second_cwd.to_string_lossy().into_owned();
|
|
|
|
|
for _ in 0..10 {
|
|
|
|
|
let notification = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_notification_message("codex/event/user_message"),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("user_message timeout")
|
|
|
|
|
.expect("user_message notification");
|
|
|
|
|
let params = notification
|
|
|
|
|
.params
|
|
|
|
|
.clone()
|
|
|
|
|
.expect("user_message should include params");
|
|
|
|
|
let event: Event = serde_json::from_value(params).expect("deserialize user_message event");
|
|
|
|
|
if let EventMsg::UserMessage(user) = event.msg
|
|
|
|
|
&& matches!(user.kind, Some(InputMessageKind::EnvironmentContext))
|
|
|
|
|
&& user.message.contains(&second_cwd_str)
|
|
|
|
|
{
|
|
|
|
|
env_message = Some(user.message);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let env_message = env_message.expect("expected environment context update");
|
|
|
|
|
assert!(
|
|
|
|
|
env_message.contains("<sandbox_mode>danger-full-access</sandbox_mode>"),
|
|
|
|
|
"env context should reflect new sandbox mode: {env_message}"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
env_message.contains("<network_access>enabled</network_access>"),
|
|
|
|
|
"env context should enable network access for danger-full-access policy: {env_message}"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
env_message.contains(&second_cwd_str),
|
|
|
|
|
"env context should include updated cwd: {env_message}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let exec_begin_notification = timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_notification_message("codex/event/exec_command_begin"),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("exec_command_begin timeout")
|
|
|
|
|
.expect("exec_command_begin notification");
|
|
|
|
|
let params = exec_begin_notification
|
|
|
|
|
.params
|
|
|
|
|
.clone()
|
|
|
|
|
.expect("exec_command_begin params");
|
|
|
|
|
let event: Event = serde_json::from_value(params).expect("deserialize exec begin event");
|
|
|
|
|
let exec_begin = match event.msg {
|
|
|
|
|
EventMsg::ExecCommandBegin(exec_begin) => exec_begin,
|
|
|
|
|
other => panic!("expected ExecCommandBegin event, got {other:?}"),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
exec_begin.cwd, second_cwd,
|
|
|
|
|
"exec turn should run from updated cwd"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
exec_begin.command,
|
|
|
|
|
vec![
|
|
|
|
|
"bash".to_string(),
|
|
|
|
|
"-lc".to_string(),
|
|
|
|
|
"echo second turn".to_string()
|
|
|
|
|
],
|
|
|
|
|
"exec turn should run expected command"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
timeout(
|
|
|
|
|
DEFAULT_READ_TIMEOUT,
|
|
|
|
|
mcp.read_stream_until_notification_message("codex/event/task_complete"),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.expect("task_complete 2 timeout")
|
|
|
|
|
.expect("task_complete 2 notification");
|
|
|
|
|
}
|
|
|
|
|
|
feat: support traditional JSON-RPC request/response in MCP server (#2264)
This introduces a new set of request types that our `codex mcp`
supports. Note that these do not conform to MCP tool calls so that
instead of having to send something like this:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 42,
"params": {
"name": "newConversation",
"arguments": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
}
```
we can send something like this:
```json
{
"jsonrpc": "2.0",
"method": "newConversation",
"id": 42,
"params": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
```
Admittedly, this new format is not a valid MCP tool call, but we are OK
with that right now. (That is, not everything we might want to request
of `codex mcp` is something that is appropriate for an autonomous agent
to do.)
To start, this introduces four request types:
- `newConversation`
- `sendUserMessage`
- `addConversationListener`
- `removeConversationListener`
The new `mcp-server/tests/codex_message_processor_flow.rs` shows how
these can be used.
The types are defined on the `CodexRequest` enum, so we introduce a new
`CodexMessageProcessor` that is responsible for dealing with requests
from this enum. The top-level `MessageProcessor` has been updated so
that when `process_request()` is called, it first checks whether the
request conforms to `CodexRequest` and dispatches it to
`CodexMessageProcessor` if so.
Note that I also decided to use `camelCase` for the on-the-wire format,
as that seems to be the convention for MCP.
For the moment, the new protocol is defined in `wire_format.rs` within
the `mcp-server` crate, but in a subsequent PR, I will probably move it
to its own crate to ensure the protocol has minimal dependencies and
that we can codegen a schema from it.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2264).
* #2278
* __->__ #2264
2025-08-13 17:36:29 -07:00
|
|
|
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
|
|
|
|
|
let config_toml = codex_home.join("config.toml");
|
|
|
|
|
std::fs::write(
|
|
|
|
|
config_toml,
|
|
|
|
|
format!(
|
|
|
|
|
r#"
|
|
|
|
|
model = "mock-model"
|
2025-08-15 10:05:58 -07:00
|
|
|
approval_policy = "untrusted"
|
feat: support traditional JSON-RPC request/response in MCP server (#2264)
This introduces a new set of request types that our `codex mcp`
supports. Note that these do not conform to MCP tool calls so that
instead of having to send something like this:
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 42,
"params": {
"name": "newConversation",
"arguments": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
}
```
we can send something like this:
```json
{
"jsonrpc": "2.0",
"method": "newConversation",
"id": 42,
"params": {
"model": "gpt-5",
"approvalPolicy": "on-request"
}
}
```
Admittedly, this new format is not a valid MCP tool call, but we are OK
with that right now. (That is, not everything we might want to request
of `codex mcp` is something that is appropriate for an autonomous agent
to do.)
To start, this introduces four request types:
- `newConversation`
- `sendUserMessage`
- `addConversationListener`
- `removeConversationListener`
The new `mcp-server/tests/codex_message_processor_flow.rs` shows how
these can be used.
The types are defined on the `CodexRequest` enum, so we introduce a new
`CodexMessageProcessor` that is responsible for dealing with requests
from this enum. The top-level `MessageProcessor` has been updated so
that when `process_request()` is called, it first checks whether the
request conforms to `CodexRequest` and dispatches it to
`CodexMessageProcessor` if so.
Note that I also decided to use `camelCase` for the on-the-wire format,
as that seems to be the convention for MCP.
For the moment, the new protocol is defined in `wire_format.rs` within
the `mcp-server` crate, but in a subsequent PR, I will probably move it
to its own crate to ensure the protocol has minimal dependencies and
that we can codegen a schema from it.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2264).
* #2278
* __->__ #2264
2025-08-13 17:36:29 -07:00
|
|
|
|
|
|
|
|
model_provider = "mock_provider"
|
|
|
|
|
|
|
|
|
|
[model_providers.mock_provider]
|
|
|
|
|
name = "Mock provider for test"
|
|
|
|
|
base_url = "{server_uri}/v1"
|
|
|
|
|
wire_api = "chat"
|
|
|
|
|
request_max_retries = 0
|
|
|
|
|
stream_max_retries = 0
|
|
|
|
|
"#
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|