mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
We started working with MCP in Codex before https://crates.io/crates/rmcp was mature, so we had our own crate for MCP types that was generated from the MCP schema: https://github.com/openai/codex/blob/8b95d3e082376f4cb23e92641705a22afb28a9da/codex-rs/mcp-types/README.md Now that `rmcp` is more mature, it makes more sense to use their MCP types in Rust, as they handle details (like the `_meta` field) that our custom version ignored. Though one advantage that our custom types had is that our generated types implemented `JsonSchema` and `ts_rs::TS`, whereas the types in `rmcp` do not. As such, part of the work of this PR is leveraging the adapters between `rmcp` types and the serializable types that are API for us (app server and MCP) introduced in #10356. Note this PR results in a number of changes to `codex-rs/app-server-protocol/schema`, which merit special attention during review. We must ensure that these changes are still backwards-compatible, which is possible because we have: ```diff - export type CallToolResult = { content: Array<ContentBlock>, isError?: boolean, structuredContent?: JsonValue, }; + export type CallToolResult = { content: Array<JsonValue>, structuredContent?: JsonValue, isError?: boolean, _meta?: JsonValue, }; ``` so `ContentBlock` has been replaced with the more general `JsonValue`. Note that `ContentBlock` was defined as: ```typescript export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource; ``` so the deletion of those individual variants should not be a cause of great concern. Similarly, we have the following change in `codex-rs/app-server-protocol/schema/typescript/Tool.ts`: ``` - export type Tool = { annotations?: ToolAnnotations, description?: string, inputSchema: ToolInputSchema, name: string, outputSchema?: ToolOutputSchema, title?: string, }; + export type Tool = { name: string, title?: string, description?: string, inputSchema: JsonValue, outputSchema?: JsonValue, annotations?: JsonValue, icons?: Array<JsonValue>, _meta?: JsonValue, }; ``` so: - `annotations?: ToolAnnotations` ➡️ `JsonValue` - `inputSchema: ToolInputSchema` ➡️ `JsonValue` - `outputSchema?: ToolOutputSchema` ➡️ `JsonValue` and two new fields: `icons?: Array<JsonValue>, _meta?: JsonValue` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/10349). * #10357 * __->__ #10349 * #10356
155 lines
5.6 KiB
Rust
155 lines
5.6 KiB
Rust
//! Prototype MCP server.
|
||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||
|
||
use std::io::ErrorKind;
|
||
use std::io::Result as IoResult;
|
||
use std::path::PathBuf;
|
||
|
||
use codex_common::CliConfigOverrides;
|
||
use codex_core::config::Config;
|
||
|
||
use rmcp::model::ClientNotification;
|
||
use rmcp::model::ClientRequest;
|
||
use rmcp::model::JsonRpcMessage;
|
||
use serde_json::Value;
|
||
use tokio::io::AsyncBufReadExt;
|
||
use tokio::io::AsyncWriteExt;
|
||
use tokio::io::BufReader;
|
||
use tokio::io::{self};
|
||
use tokio::sync::mpsc;
|
||
use tracing::debug;
|
||
use tracing::error;
|
||
use tracing::info;
|
||
use tracing_subscriber::EnvFilter;
|
||
|
||
mod codex_tool_config;
|
||
mod codex_tool_runner;
|
||
mod exec_approval;
|
||
pub(crate) mod message_processor;
|
||
mod outgoing_message;
|
||
mod patch_approval;
|
||
|
||
use crate::message_processor::MessageProcessor;
|
||
use crate::outgoing_message::OutgoingJsonRpcMessage;
|
||
use crate::outgoing_message::OutgoingMessage;
|
||
use crate::outgoing_message::OutgoingMessageSender;
|
||
|
||
pub use crate::codex_tool_config::CodexToolCallParam;
|
||
pub use crate::codex_tool_config::CodexToolCallReplyParam;
|
||
pub use crate::exec_approval::ExecApprovalElicitRequestParams;
|
||
pub use crate::exec_approval::ExecApprovalResponse;
|
||
pub use crate::patch_approval::PatchApprovalElicitRequestParams;
|
||
pub use crate::patch_approval::PatchApprovalResponse;
|
||
|
||
/// Size of the bounded channels used to communicate between tasks. The value
|
||
/// is a balance between throughput and memory usage – 128 messages should be
|
||
/// plenty for an interactive CLI.
|
||
const CHANNEL_CAPACITY: usize = 128;
|
||
|
||
type IncomingMessage = JsonRpcMessage<ClientRequest, Value, ClientNotification>;
|
||
|
||
pub async fn run_main(
|
||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||
cli_config_overrides: CliConfigOverrides,
|
||
) -> IoResult<()> {
|
||
// Install a simple subscriber so `tracing` output is visible. Users can
|
||
// control the log level with `RUST_LOG`.
|
||
tracing_subscriber::fmt()
|
||
.with_writer(std::io::stderr)
|
||
.with_env_filter(EnvFilter::from_default_env())
|
||
.init();
|
||
|
||
// Set up channels.
|
||
let (incoming_tx, mut incoming_rx) = mpsc::channel::<IncomingMessage>(CHANNEL_CAPACITY);
|
||
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::<OutgoingMessage>();
|
||
|
||
// Task: read from stdin, push to `incoming_tx`.
|
||
let stdin_reader_handle = tokio::spawn({
|
||
async move {
|
||
let stdin = io::stdin();
|
||
let reader = BufReader::new(stdin);
|
||
let mut lines = reader.lines();
|
||
|
||
while let Some(line) = lines.next_line().await.unwrap_or_default() {
|
||
match serde_json::from_str::<IncomingMessage>(&line) {
|
||
Ok(msg) => {
|
||
if incoming_tx.send(msg).await.is_err() {
|
||
// Receiver gone – nothing left to do.
|
||
break;
|
||
}
|
||
}
|
||
Err(e) => error!("Failed to deserialize JSON-RPC message: {e}"),
|
||
}
|
||
}
|
||
|
||
debug!("stdin reader finished (EOF)");
|
||
}
|
||
});
|
||
|
||
// Parse CLI overrides once and derive the base Config eagerly so later
|
||
// components do not need to work with raw TOML values.
|
||
let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| {
|
||
std::io::Error::new(
|
||
ErrorKind::InvalidInput,
|
||
format!("error parsing -c overrides: {e}"),
|
||
)
|
||
})?;
|
||
let config = Config::load_with_cli_overrides(cli_kv_overrides)
|
||
.await
|
||
.map_err(|e| {
|
||
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
|
||
})?;
|
||
|
||
// Task: process incoming messages.
|
||
let processor_handle = tokio::spawn({
|
||
let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx);
|
||
let mut processor = MessageProcessor::new(
|
||
outgoing_message_sender,
|
||
codex_linux_sandbox_exe,
|
||
std::sync::Arc::new(config),
|
||
);
|
||
async move {
|
||
while let Some(msg) = incoming_rx.recv().await {
|
||
match msg {
|
||
JsonRpcMessage::Request(r) => processor.process_request(r).await,
|
||
JsonRpcMessage::Response(r) => processor.process_response(r).await,
|
||
JsonRpcMessage::Notification(n) => processor.process_notification(n).await,
|
||
JsonRpcMessage::Error(e) => processor.process_error(e),
|
||
}
|
||
}
|
||
|
||
info!("processor task exited (channel closed)");
|
||
}
|
||
});
|
||
|
||
// Task: write outgoing messages to stdout.
|
||
let stdout_writer_handle = tokio::spawn(async move {
|
||
let mut stdout = io::stdout();
|
||
while let Some(outgoing_message) = outgoing_rx.recv().await {
|
||
let msg: OutgoingJsonRpcMessage = outgoing_message.into();
|
||
match serde_json::to_string(&msg) {
|
||
Ok(json) => {
|
||
if let Err(e) = stdout.write_all(json.as_bytes()).await {
|
||
error!("Failed to write to stdout: {e}");
|
||
break;
|
||
}
|
||
if let Err(e) = stdout.write_all(b"\n").await {
|
||
error!("Failed to write newline to stdout: {e}");
|
||
break;
|
||
}
|
||
}
|
||
Err(e) => error!("Failed to serialize JSON-RPC message: {e}"),
|
||
}
|
||
}
|
||
|
||
info!("stdout writer exited (channel closed)");
|
||
});
|
||
|
||
// Wait for all tasks to finish. The typical exit path is the stdin reader
|
||
// hitting EOF which, once it drops `incoming_tx`, propagates shutdown to
|
||
// the processor and then to the stdout task.
|
||
let _ = tokio::join!(stdin_reader_handle, processor_handle, stdout_writer_handle);
|
||
|
||
Ok(())
|
||
}
|