Compare commits

...

1 Commits

Author SHA1 Message Date
Matthew Zeng
90e9ad5ffb update 2026-01-31 23:36:16 -08:00
23 changed files with 5491 additions and 221 deletions

View File

@@ -70,10 +70,9 @@ use futures::future::BoxFuture;
use futures::prelude::*;
use futures::stream::FuturesOrdered;
use mcp_types::CallToolResult;
use mcp_types::ListResourceTemplatesRequestParams;
use mcp_types::ListResourceTemplatesResult;
use mcp_types::ListResourcesRequestParams;
use mcp_types::ListResourcesResult;
use mcp_types::PaginatedRequestParams;
use mcp_types::ReadResourceRequestParams;
use mcp_types::ReadResourceResult;
use mcp_types::RequestId;
@@ -2196,7 +2195,7 @@ impl Session {
pub async fn list_resources(
&self,
server: &str,
params: Option<ListResourcesRequestParams>,
params: Option<PaginatedRequestParams>,
) -> anyhow::Result<ListResourcesResult> {
self.services
.mcp_connection_manager
@@ -2209,7 +2208,7 @@ impl Session {
pub async fn list_resource_templates(
&self,
server: &str,
params: Option<ListResourceTemplatesRequestParams>,
params: Option<PaginatedRequestParams>,
) -> anyhow::Result<ListResourceTemplatesResult> {
self.services
.mcp_connection_manager
@@ -5108,6 +5107,7 @@ mod tests {
#[test]
fn prefers_structured_content_when_present() {
let ctr = CallToolResult {
_meta: None,
// Content present but should be ignored because structured_content is set.
content: vec![text_block("ignored")],
is_error: None,
@@ -5154,6 +5154,7 @@ mod tests {
#[test]
fn falls_back_to_content_when_structured_is_null() {
let ctr = CallToolResult {
_meta: None,
content: vec![text_block("hello"), text_block("world")],
is_error: None,
structured_content: Some(serde_json::Value::Null),
@@ -5173,6 +5174,7 @@ mod tests {
#[test]
fn success_flag_reflects_is_error_true() {
let ctr = CallToolResult {
_meta: None,
content: vec![text_block("unused")],
is_error: Some(true),
structured_content: Some(json!({ "message": "bad" })),
@@ -5191,6 +5193,7 @@ mod tests {
#[test]
fn success_flag_true_with_no_error_and_content_used() {
let ctr = CallToolResult {
_meta: None,
content: vec![text_block("alpha")],
is_error: Some(false),
structured_content: None,
@@ -5246,6 +5249,7 @@ mod tests {
fn text_block(s: &str) -> ContentBlock {
ContentBlock::TextContent(TextContent {
_meta: None,
annotations: None,
text: s.to_string(),
r#type: "text".to_string(),

View File

@@ -249,9 +249,13 @@ mod tests {
fn make_tool(name: &str) -> McpTool {
McpTool {
_meta: None,
annotations: None,
description: None,
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: None,
required: None,
r#type: "object".to_string(),

View File

@@ -38,11 +38,11 @@ use futures::future::BoxFuture;
use futures::future::FutureExt;
use futures::future::Shared;
use mcp_types::ClientCapabilities;
use mcp_types::ClientCapabilitiesElicitation;
use mcp_types::Implementation;
use mcp_types::ListResourceTemplatesRequestParams;
use mcp_types::ListResourceTemplatesResult;
use mcp_types::ListResourcesRequestParams;
use mcp_types::ListResourcesResult;
use mcp_types::PaginatedRequestParams;
use mcp_types::ReadResourceRequestParams;
use mcp_types::ReadResourceResult;
use mcp_types::RequestId;
@@ -52,7 +52,6 @@ use mcp_types::Tool;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
use sha1::Digest;
use sha1::Sha1;
use tokio::sync::Mutex;
@@ -493,7 +492,8 @@ impl McpConnectionManager {
let mut cursor: Option<String> = None;
loop {
let params = cursor.as_ref().map(|next| ListResourcesRequestParams {
let params = cursor.as_ref().map(|next| PaginatedRequestParams {
_meta: None,
cursor: Some(next.clone()),
});
let response = match client.list_resources(params, timeout).await {
@@ -558,11 +558,10 @@ impl McpConnectionManager {
let mut cursor: Option<String> = None;
loop {
let params = cursor
.as_ref()
.map(|next| ListResourceTemplatesRequestParams {
cursor: Some(next.clone()),
});
let params = cursor.as_ref().map(|next| PaginatedRequestParams {
_meta: None,
cursor: Some(next.clone()),
});
let response = match client.list_resource_templates(params, timeout).await {
Ok(result) => result,
Err(err) => return (server_name_cloned, Err(err)),
@@ -634,7 +633,7 @@ impl McpConnectionManager {
pub async fn list_resources(
&self,
server: &str,
params: Option<ListResourcesRequestParams>,
params: Option<PaginatedRequestParams>,
) -> Result<ListResourcesResult> {
let managed = self.client_by_name(server).await?;
let timeout = managed.tool_timeout;
@@ -650,7 +649,7 @@ impl McpConnectionManager {
pub async fn list_resource_templates(
&self,
server: &str,
params: Option<ListResourceTemplatesRequestParams>,
params: Option<PaginatedRequestParams>,
) -> Result<ListResourceTemplatesResult> {
let managed = self.client_by_name(server).await?;
let client = managed.client.clone();
@@ -850,18 +849,24 @@ async fn start_server_task(
elicitation_requests: ElicitationRequestManager,
) -> Result<ManagedClient, StartupOutcomeError> {
let params = mcp_types::InitializeRequestParams {
_meta: None,
capabilities: ClientCapabilities {
elicitation: Some(ClientCapabilitiesElicitation {
form: None,
url: None,
}),
experimental: None,
roots: None,
sampling: None,
// https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#capabilities
// indicates this should be an empty object.
elicitation: Some(json!({})),
tasks: None,
},
client_info: Implementation {
description: None,
icons: None,
name: "codex-mcp-client".to_owned(),
version: env!("CARGO_PKG_VERSION").to_owned(),
title: Some("Codex".into()),
website_url: None,
// This field is used by Codex when it is an MCP
// server: it should not be used when Codex is
// an MCP client.
@@ -1055,9 +1060,13 @@ mod tests {
server_name: server_name.to_string(),
tool_name: tool_name.to_string(),
tool: Tool {
_meta: None,
annotations: None,
description: Some(format!("Test tool: {tool_name}")),
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: None,
required: None,
r#type: "object".to_string(),

View File

@@ -6,10 +6,9 @@ use std::time::Instant;
use async_trait::async_trait;
use mcp_types::CallToolResult;
use mcp_types::ContentBlock;
use mcp_types::ListResourceTemplatesRequestParams;
use mcp_types::ListResourceTemplatesResult;
use mcp_types::ListResourcesRequestParams;
use mcp_types::ListResourcesResult;
use mcp_types::PaginatedRequestParams;
use mcp_types::ReadResourceRequestParams;
use mcp_types::ReadResourceResult;
use mcp_types::Resource;
@@ -264,7 +263,8 @@ async fn handle_list_resources(
let payload_result: Result<ListResourcesPayload, FunctionCallError> = async {
if let Some(server_name) = server.clone() {
let params = cursor.clone().map(|value| ListResourcesRequestParams {
let params = cursor.clone().map(|value| PaginatedRequestParams {
_meta: None,
cursor: Some(value),
});
let result = session
@@ -371,11 +371,10 @@ async fn handle_list_resource_templates(
let payload_result: Result<ListResourceTemplatesPayload, FunctionCallError> = async {
if let Some(server_name) = server.clone() {
let params = cursor
.clone()
.map(|value| ListResourceTemplatesRequestParams {
cursor: Some(value),
});
let params = cursor.clone().map(|value| PaginatedRequestParams {
_meta: None,
cursor: Some(value),
});
let result = session
.list_resource_templates(&server_name, params)
.await
@@ -482,7 +481,13 @@ async fn handle_read_resource(
let payload_result: Result<ReadResourcePayload, FunctionCallError> = async {
let result = session
.read_resource(&server, ReadResourceRequestParams { uri: uri.clone() })
.read_resource(
&server,
ReadResourceRequestParams {
_meta: None,
uri: uri.clone(),
},
)
.await
.map_err(|err| {
FunctionCallError::RespondToModel(format!("resources/read failed: {err:#}"))
@@ -551,7 +556,9 @@ async fn handle_read_resource(
fn call_tool_result_from_content(content: &str, success: Option<bool>) -> CallToolResult {
CallToolResult {
_meta: None,
content: vec![ContentBlock::TextContent(TextContent {
_meta: None,
annotations: None,
text: content.to_string(),
r#type: "text".to_string(),
@@ -685,8 +692,10 @@ mod tests {
fn resource(uri: &str, name: &str) -> Resource {
Resource {
_meta: None,
annotations: None,
description: None,
icons: None,
mime_type: None,
name: name.to_string(),
size: None,
@@ -697,8 +706,10 @@ mod tests {
fn template(uri_template: &str, name: &str) -> ResourceTemplate {
ResourceTemplate {
_meta: None,
annotations: None,
description: None,
icons: None,
mime_type: None,
name: name.to_string(),
title: None,
@@ -719,6 +730,7 @@ mod tests {
#[test]
fn list_resources_payload_from_single_server_copies_next_cursor() {
let result = ListResourcesResult {
_meta: None,
next_cursor: Some("cursor-1".to_string()),
resources: vec![resource("memo://id", "memo")],
};

View File

@@ -2027,8 +2027,12 @@ mod tests {
Some(HashMap::from([(
"test_server/do_something_cool".to_string(),
mcp_types::Tool {
_meta: None,
name: "do_something_cool".to_string(),
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: Some(serde_json::json!({
"string_argument": {
"type": "string",
@@ -2124,8 +2128,12 @@ mod tests {
(
"test_server/do".to_string(),
mcp_types::Tool {
_meta: None,
name: "a".to_string(),
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: Some(serde_json::json!({})),
required: None,
r#type: "object".to_string(),
@@ -2139,8 +2147,12 @@ mod tests {
(
"test_server/something".to_string(),
mcp_types::Tool {
_meta: None,
name: "b".to_string(),
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: Some(serde_json::json!({})),
required: None,
r#type: "object".to_string(),
@@ -2154,8 +2166,12 @@ mod tests {
(
"test_server/cool".to_string(),
mcp_types::Tool {
_meta: None,
name: "c".to_string(),
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: Some(serde_json::json!({})),
required: None,
r#type: "object".to_string(),
@@ -2201,8 +2217,12 @@ mod tests {
Some(HashMap::from([(
"dash/search".to_string(),
mcp_types::Tool {
_meta: None,
name: "search".to_string(),
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: Some(serde_json::json!({
"query": {
"description": "search query"
@@ -2259,8 +2279,12 @@ mod tests {
Some(HashMap::from([(
"dash/paginate".to_string(),
mcp_types::Tool {
_meta: None,
name: "paginate".to_string(),
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: Some(serde_json::json!({
"page": { "type": "integer" }
})),
@@ -2314,8 +2338,12 @@ mod tests {
Some(HashMap::from([(
"dash/tags".to_string(),
mcp_types::Tool {
_meta: None,
name: "tags".to_string(),
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: Some(serde_json::json!({
"tags": { "type": "array" }
})),
@@ -2371,8 +2399,12 @@ mod tests {
Some(HashMap::from([(
"dash/value".to_string(),
mcp_types::Tool {
_meta: None,
name: "value".to_string(),
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: Some(serde_json::json!({
"value": { "anyOf": [ { "type": "string" }, { "type": "number" } ] }
})),
@@ -2483,8 +2515,12 @@ Examples of valid command strings:
Some(HashMap::from([(
"test_server/do_something_cool".to_string(),
mcp_types::Tool {
_meta: None,
name: "do_something_cool".to_string(),
execution: None,
icons: None,
input_schema: ToolInputSchema {
schema: None,
properties: Some(serde_json::json!({
"string_argument": {
"type": "string",

View File

@@ -125,6 +125,7 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
});
Tool {
_meta: None,
name: "codex".to_string(),
title: Some("Codex".to_string()),
input_schema: tool_input_schema,
@@ -133,11 +134,14 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
"Run a Codex session. Accepts configuration parameters matching the Codex Config struct.".to_string(),
),
annotations: None,
execution: None,
icons: None,
}
}
fn codex_tool_output_schema() -> ToolOutputSchema {
ToolOutputSchema {
schema: None,
properties: Some(serde_json::json!({
"threadId": { "type": "string" },
"content": { "type": "string" }
@@ -247,6 +251,7 @@ pub(crate) fn create_tool_for_codex_tool_call_reply_param() -> Tool {
});
Tool {
_meta: None,
name: "codex-reply".to_string(),
title: Some("Codex Reply".to_string()),
input_schema: tool_input_schema,
@@ -255,6 +260,8 @@ pub(crate) fn create_tool_for_codex_tool_call_reply_param() -> Tool {
"Continue a Codex conversation by providing the thread id and prompt.".to_string(),
),
annotations: None,
execution: None,
icons: None,
}
}
@@ -281,6 +288,7 @@ mod tests {
let expected_tool_json = serde_json::json!({
"description": "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
"properties": {
"approval-policy": {
"description": "Approval policy for shell commands generated by the model: `untrusted`, `on-failure`, `on-request`, `never`.",
@@ -368,6 +376,7 @@ mod tests {
let expected_tool_json = serde_json::json!({
"description": "Continue a Codex conversation by providing the thread id and prompt.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2019-09/schema",
"properties": {
"conversationId": {
"description": "DEPRECATED: use threadId instead.",

View File

@@ -43,6 +43,7 @@ pub(crate) fn create_call_tool_result_with_thread_id(
) -> CallToolResult {
let content_text = text;
let content = vec![ContentBlock::TextContent(TextContent {
_meta: None,
r#type: "text".to_string(),
text: content_text.clone(),
annotations: None,
@@ -52,6 +53,7 @@ pub(crate) fn create_call_tool_result_with_thread_id(
"content": content_text,
});
CallToolResult {
_meta: None,
content,
is_error,
structured_content: Some(structured_content),
@@ -78,7 +80,9 @@ pub async fn run_codex_tool_session(
Ok(res) => res,
Err(e) => {
let result = CallToolResult {
_meta: None,
content: vec![ContentBlock::TextContent(TextContent {
_meta: None,
r#type: "text".to_string(),
text: format!("Failed to start Codex session: {e}"),
annotations: None,

View File

@@ -7,8 +7,8 @@ use codex_core::protocol::ReviewDecision;
use codex_protocol::ThreadId;
use codex_protocol::parse_command::ParsedCommand;
use mcp_types::ElicitRequest;
use mcp_types::ElicitRequestParamsRequestedSchema;
use mcp_types::JSONRPCErrorError;
use mcp_types::ElicitRequestFormParamsRequestedSchema;
use mcp_types::Error;
use mcp_types::ModelContextProtocolRequest;
use mcp_types::RequestId;
use serde::Deserialize;
@@ -27,7 +27,7 @@ pub struct ExecApprovalElicitRequestParams {
pub message: String,
#[serde(rename = "requestedSchema")]
pub requested_schema: ElicitRequestParamsRequestedSchema,
pub requested_schema: ElicitRequestFormParamsRequestedSchema,
// These are additional fields the client can use to
// correlate the request with the codex tool call.
@@ -73,7 +73,8 @@ pub(crate) async fn handle_exec_approval_request(
let params = ExecApprovalElicitRequestParams {
message,
requested_schema: ElicitRequestParamsRequestedSchema {
requested_schema: ElicitRequestFormParamsRequestedSchema {
schema: None,
r#type: "object".to_string(),
properties: json!({}),
required: None,
@@ -96,7 +97,7 @@ pub(crate) async fn handle_exec_approval_request(
outgoing
.send_error(
request_id.clone(),
JSONRPCErrorError {
Error {
code: INVALID_PARAMS_ERROR_CODE,
message,
data: None,

View File

@@ -107,9 +107,9 @@ pub async fn run_main(
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::ResultResponse(r) => processor.process_response(r).await,
JSONRPCMessage::Notification(n) => processor.process_notification(n).await,
JSONRPCMessage::Error(e) => processor.process_error(e),
JSONRPCMessage::ErrorResponse(e) => processor.process_error(e),
}
}

View File

@@ -20,11 +20,11 @@ use mcp_types::CallToolRequestParams;
use mcp_types::CallToolResult;
use mcp_types::ClientRequest as McpClientRequest;
use mcp_types::ContentBlock;
use mcp_types::JSONRPCError;
use mcp_types::JSONRPCErrorError;
use mcp_types::Error;
use mcp_types::JSONRPCErrorResponse;
use mcp_types::JSONRPCNotification;
use mcp_types::JSONRPCRequest;
use mcp_types::JSONRPCResponse;
use mcp_types::JSONRPCResultResponse;
use mcp_types::ListToolsResult;
use mcp_types::ModelContextProtocolRequest;
use mcp_types::RequestId;
@@ -125,13 +125,29 @@ impl MessageProcessor {
McpClientRequest::CompleteRequest(params) => {
self.handle_complete(params);
}
McpClientRequest::GetTaskRequest(_) => {
self.handle_unsupported_request(request_id, "tasks/get")
.await;
}
McpClientRequest::GetTaskPayloadRequest(_) => {
self.handle_unsupported_request(request_id, "tasks/result")
.await;
}
McpClientRequest::CancelTaskRequest(_) => {
self.handle_unsupported_request(request_id, "tasks/cancel")
.await;
}
McpClientRequest::ListTasksRequest(_) => {
self.handle_unsupported_request(request_id, "tasks/list")
.await;
}
}
}
/// Handle a standalone JSON-RPC response originating from the peer.
pub(crate) async fn process_response(&mut self, response: JSONRPCResponse) {
pub(crate) async fn process_response(&mut self, response: JSONRPCResultResponse) {
tracing::info!("<- response: {:?}", response);
let JSONRPCResponse { id, result, .. } = response;
let JSONRPCResultResponse { id, result, .. } = response;
self.outgoing.notify_client_response(id, result).await
}
@@ -169,11 +185,17 @@ impl MessageProcessor {
ServerNotification::LoggingMessageNotification(params) => {
self.handle_logging_message(params);
}
ServerNotification::TaskStatusNotification(params) => {
self.handle_task_status_notification(params);
}
ServerNotification::ElicitationCompleteNotification(params) => {
self.handle_elicitation_complete_notification(params);
}
}
}
/// Handle an error object received from the peer.
pub(crate) fn process_error(&mut self, err: JSONRPCError) {
pub(crate) fn process_error(&mut self, err: JSONRPCErrorResponse) {
tracing::error!("<- error: {:?}", err);
}
@@ -186,7 +208,7 @@ impl MessageProcessor {
if self.initialized {
// Already initialised: send JSON-RPC error response.
let error = JSONRPCErrorError {
let error = Error {
code: INVALID_REQUEST_ERROR_CODE,
message: "initialize called more than once".to_string(),
data: None,
@@ -207,12 +229,14 @@ impl MessageProcessor {
// Build a minimal InitializeResult. Fill with placeholders.
let result = mcp_types::InitializeResult {
_meta: None,
capabilities: mcp_types::ServerCapabilities {
completions: None,
experimental: None,
logging: None,
prompts: None,
resources: None,
tasks: None,
tools: Some(ServerCapabilitiesTools {
list_changed: Some(true),
}),
@@ -220,9 +244,12 @@ impl MessageProcessor {
instructions: None,
protocol_version: params.protocol_version.clone(),
server_info: mcp_types::Implementation {
description: None,
icons: None,
name: "codex-mcp-server".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
title: Some("Codex".to_string()),
website_url: None,
user_agent: Some(get_codex_user_agent()),
},
};
@@ -238,6 +265,19 @@ impl MessageProcessor {
self.outgoing.send_response(id, result).await;
}
async fn handle_unsupported_request(&self, id: RequestId, method: &str) {
self.outgoing
.send_error(
id,
Error {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("method `{method}` is not supported"),
data: None,
},
)
.await;
}
async fn handle_ping(
&self,
id: RequestId,
@@ -306,6 +346,7 @@ impl MessageProcessor {
) {
tracing::trace!("tools/list -> {params:?}");
let result = ListToolsResult {
_meta: None,
tools: vec![
create_tool_for_codex_tool_call_param(),
create_tool_for_codex_tool_call_reply_param(),
@@ -323,7 +364,9 @@ impl MessageProcessor {
params: <mcp_types::CallToolRequest as mcp_types::ModelContextProtocolRequest>::Params,
) {
tracing::info!("tools/call -> params: {:?}", params);
let CallToolRequestParams { name, arguments } = params;
let CallToolRequestParams {
name, arguments, ..
} = params;
match name.as_str() {
"codex" => self.handle_tool_call_codex(id, arguments).await,
@@ -333,7 +376,9 @@ impl MessageProcessor {
}
_ => {
let result = CallToolResult {
_meta: None,
content: vec![ContentBlock::TextContent(TextContent {
_meta: None,
r#type: "text".to_string(),
text: format!("Unknown tool '{name}'"),
annotations: None,
@@ -356,7 +401,9 @@ impl MessageProcessor {
Ok(cfg) => cfg,
Err(e) => {
let result = CallToolResult {
_meta: None,
content: vec![ContentBlock::TextContent(TextContent {
_meta: None,
r#type: "text".to_owned(),
text: format!(
"Failed to load Codex configuration from overrides: {e}"
@@ -373,7 +420,9 @@ impl MessageProcessor {
},
Err(e) => {
let result = CallToolResult {
_meta: None,
content: vec![ContentBlock::TextContent(TextContent {
_meta: None,
r#type: "text".to_owned(),
text: format!("Failed to parse configuration for Codex tool: {e}"),
annotations: None,
@@ -388,7 +437,9 @@ impl MessageProcessor {
},
None => {
let result = CallToolResult {
_meta: None,
content: vec![ContentBlock::TextContent(TextContent {
_meta: None,
r#type: "text".to_string(),
text:
"Missing arguments for codex tool-call; the `prompt` field is required."
@@ -439,7 +490,9 @@ impl MessageProcessor {
Err(e) => {
tracing::error!("Failed to parse Codex tool call reply parameters: {e}");
let result = CallToolResult {
_meta: None,
content: vec![ContentBlock::TextContent(TextContent {
_meta: None,
r#type: "text".to_owned(),
text: format!("Failed to parse configuration for Codex tool: {e}"),
annotations: None,
@@ -457,7 +510,9 @@ impl MessageProcessor {
"Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required."
);
let result = CallToolResult {
_meta: None,
content: vec![ContentBlock::TextContent(TextContent {
_meta: None,
r#type: "text".to_owned(),
text: "Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required.".to_owned(),
annotations: None,
@@ -476,7 +531,9 @@ impl MessageProcessor {
Err(e) => {
tracing::error!("Failed to parse thread_id: {e}");
let result = CallToolResult {
_meta: None,
content: vec![ContentBlock::TextContent(TextContent {
_meta: None,
r#type: "text".to_owned(),
text: format!("Failed to parse thread_id: {e}"),
annotations: None,
@@ -550,7 +607,10 @@ impl MessageProcessor {
&self,
params: <mcp_types::CancelledNotification as mcp_types::ModelContextProtocolNotification>::Params,
) {
let request_id = params.request_id;
let Some(request_id) = params.request_id else {
tracing::warn!("notifications/cancelled received without requestId");
return;
};
// Create a stable string form early for logging and submission id.
let request_id_string = match &request_id {
RequestId::String(s) => s.clone(),
@@ -641,4 +701,18 @@ impl MessageProcessor {
) {
tracing::info!("notifications/message -> params: {:?}", params);
}
fn handle_task_status_notification(
&self,
params: <mcp_types::TaskStatusNotification as mcp_types::ModelContextProtocolNotification>::Params,
) {
tracing::info!("notifications/tasks/status -> params: {:?}", params);
}
fn handle_elicitation_complete_notification(
&self,
params: <mcp_types::ElicitationCompleteNotification as mcp_types::ModelContextProtocolNotification>::Params,
) {
tracing::info!("notifications/elicitation/complete -> params: {:?}", params);
}
}

View File

@@ -4,13 +4,13 @@ use std::sync::atomic::Ordering;
use codex_core::protocol::Event;
use codex_protocol::ThreadId;
use mcp_types::Error;
use mcp_types::JSONRPC_VERSION;
use mcp_types::JSONRPCError;
use mcp_types::JSONRPCErrorError;
use mcp_types::JSONRPCErrorResponse;
use mcp_types::JSONRPCMessage;
use mcp_types::JSONRPCNotification;
use mcp_types::JSONRPCRequest;
use mcp_types::JSONRPCResponse;
use mcp_types::JSONRPCResultResponse;
use mcp_types::RequestId;
use mcp_types::Result;
use serde::Serialize;
@@ -86,7 +86,7 @@ impl OutgoingMessageSender {
Err(err) => {
self.send_error(
id,
JSONRPCErrorError {
Error {
code: INTERNAL_ERROR_CODE,
message: format!("failed to serialize response: {err}"),
data: None,
@@ -130,7 +130,7 @@ impl OutgoingMessageSender {
let _ = self.sender.send(outgoing_message);
}
pub(crate) async fn send_error(&self, id: RequestId, error: JSONRPCErrorError) {
pub(crate) async fn send_error(&self, id: RequestId, error: Error) {
let outgoing_message = OutgoingMessage::Error(OutgoingError { id, error });
let _ = self.sender.send(outgoing_message);
}
@@ -164,17 +164,19 @@ impl From<OutgoingMessage> for JSONRPCMessage {
})
}
Response(OutgoingResponse { id, result }) => {
JSONRPCMessage::Response(JSONRPCResponse {
JSONRPCMessage::ResultResponse(JSONRPCResultResponse {
jsonrpc: JSONRPC_VERSION.into(),
id,
result,
})
}
Error(OutgoingError { id, error }) => JSONRPCMessage::Error(JSONRPCError {
jsonrpc: JSONRPC_VERSION.into(),
id,
error,
}),
Error(OutgoingError { id, error }) => {
JSONRPCMessage::ErrorResponse(JSONRPCErrorResponse {
jsonrpc: JSONRPC_VERSION.into(),
id: Some(id),
error,
})
}
}
}
}
@@ -225,7 +227,7 @@ pub(crate) struct OutgoingResponse {
#[derive(Debug, Clone, PartialEq, Serialize)]
pub(crate) struct OutgoingError {
pub error: JSONRPCErrorError,
pub error: Error,
pub id: RequestId,
}

View File

@@ -8,8 +8,8 @@ use codex_core::protocol::Op;
use codex_core::protocol::ReviewDecision;
use codex_protocol::ThreadId;
use mcp_types::ElicitRequest;
use mcp_types::ElicitRequestParamsRequestedSchema;
use mcp_types::JSONRPCErrorError;
use mcp_types::ElicitRequestFormParamsRequestedSchema;
use mcp_types::Error;
use mcp_types::ModelContextProtocolRequest;
use mcp_types::RequestId;
use serde::Deserialize;
@@ -24,7 +24,7 @@ use crate::outgoing_message::OutgoingMessageSender;
pub struct PatchApprovalElicitRequestParams {
pub message: String,
#[serde(rename = "requestedSchema")]
pub requested_schema: ElicitRequestParamsRequestedSchema,
pub requested_schema: ElicitRequestFormParamsRequestedSchema,
#[serde(rename = "threadId")]
pub thread_id: ThreadId,
pub codex_elicitation: String,
@@ -64,7 +64,8 @@ pub(crate) async fn handle_patch_approval_request(
let params = PatchApprovalElicitRequestParams {
message: message_lines.join("\n"),
requested_schema: ElicitRequestParamsRequestedSchema {
requested_schema: ElicitRequestFormParamsRequestedSchema {
schema: None,
r#type: "object".to_string(),
properties: json!({}),
required: None,
@@ -87,7 +88,7 @@ pub(crate) async fn handle_patch_approval_request(
outgoing
.send_error(
request_id.clone(),
JSONRPCErrorError {
Error {
code: INVALID_PARAMS_ERROR_CODE,
message,
data: None,

View File

@@ -6,14 +6,14 @@ pub use core_test_support::format_with_current_shell;
pub use core_test_support::format_with_current_shell_display_non_login;
pub use core_test_support::format_with_current_shell_non_login;
pub use mcp_process::McpProcess;
use mcp_types::JSONRPCResponse;
use mcp_types::JSONRPCResultResponse;
pub use mock_model_server::create_mock_chat_completions_server;
pub use responses::create_apply_patch_sse_response;
pub use responses::create_final_assistant_message_sse_response;
pub use responses::create_shell_command_sse_response;
use serde::de::DeserializeOwned;
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResultResponse) -> anyhow::Result<T> {
let value = serde_json::to_value(response.result)?;
let codex_response = serde_json::from_value(value)?;
Ok(codex_response)

View File

@@ -14,13 +14,14 @@ use codex_mcp_server::CodexToolCallParam;
use mcp_types::CallToolRequestParams;
use mcp_types::ClientCapabilities;
use mcp_types::ClientCapabilitiesElicitation;
use mcp_types::Implementation;
use mcp_types::InitializeRequestParams;
use mcp_types::JSONRPC_VERSION;
use mcp_types::JSONRPCMessage;
use mcp_types::JSONRPCNotification;
use mcp_types::JSONRPCRequest;
use mcp_types::JSONRPCResponse;
use mcp_types::JSONRPCResultResponse;
use mcp_types::ModelContextProtocolNotification;
use mcp_types::ModelContextProtocolRequest;
use mcp_types::RequestId;
@@ -111,16 +112,24 @@ impl McpProcess {
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
let params = InitializeRequestParams {
_meta: None,
capabilities: ClientCapabilities {
elicitation: Some(json!({})),
elicitation: Some(ClientCapabilitiesElicitation {
form: None,
url: None,
}),
experimental: None,
roots: None,
sampling: None,
tasks: None,
},
client_info: Implementation {
description: None,
icons: None,
name: "elicitation test".into(),
title: Some("Elicitation Test".into()),
version: "0.0.0".into(),
website_url: None,
user_agent: None,
},
protocol_version: mcp_types::MCP_SCHEMA_VERSION.into(),
@@ -147,7 +156,7 @@ impl McpProcess {
codex_core::terminal::user_agent()
);
assert_eq!(
JSONRPCMessage::Response(JSONRPCResponse {
JSONRPCMessage::ResultResponse(JSONRPCResultResponse {
jsonrpc: JSONRPC_VERSION.into(),
id: RequestId::Integer(request_id),
result: json!({
@@ -186,8 +195,10 @@ impl McpProcess {
params: CodexToolCallParam,
) -> anyhow::Result<i64> {
let codex_tool_call_params = CallToolRequestParams {
_meta: None,
name: "codex".to_string(),
arguments: Some(serde_json::to_value(params)?),
task: None,
};
self.send_request(
mcp_types::CallToolRequest::METHOD,
@@ -218,7 +229,7 @@ impl McpProcess {
id: RequestId,
result: serde_json::Value,
) -> anyhow::Result<()> {
self.send_jsonrpc_message(JSONRPCMessage::Response(JSONRPCResponse {
self.send_jsonrpc_message(JSONRPCMessage::ResultResponse(JSONRPCResultResponse {
jsonrpc: JSONRPC_VERSION.into(),
id,
result,
@@ -256,11 +267,11 @@ impl McpProcess {
JSONRPCMessage::Request(jsonrpc_request) => {
return Ok(jsonrpc_request);
}
JSONRPCMessage::Error(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}");
JSONRPCMessage::ErrorResponse(_) => {
anyhow::bail!("unexpected JSONRPCMessage::ErrorResponse: {message:?}");
}
JSONRPCMessage::Response(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}");
JSONRPCMessage::ResultResponse(_) => {
anyhow::bail!("unexpected JSONRPCMessage::ResultResponse: {message:?}");
}
}
}
@@ -269,7 +280,7 @@ impl McpProcess {
pub async fn read_stream_until_response_message(
&mut self,
request_id: RequestId,
) -> anyhow::Result<JSONRPCResponse> {
) -> anyhow::Result<JSONRPCResultResponse> {
eprintln!("in read_stream_until_response_message({request_id:?})");
loop {
@@ -281,10 +292,10 @@ impl McpProcess {
JSONRPCMessage::Request(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}");
}
JSONRPCMessage::Error(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}");
JSONRPCMessage::ErrorResponse(_) => {
anyhow::bail!("unexpected JSONRPCMessage::ErrorResponse: {message:?}");
}
JSONRPCMessage::Response(jsonrpc_response) => {
JSONRPCMessage::ResultResponse(jsonrpc_response) => {
if jsonrpc_response.id == request_id {
return Ok(jsonrpc_response);
}
@@ -327,11 +338,11 @@ impl McpProcess {
JSONRPCMessage::Request(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}");
}
JSONRPCMessage::Error(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}");
JSONRPCMessage::ErrorResponse(_) => {
anyhow::bail!("unexpected JSONRPCMessage::ErrorResponse: {message:?}");
}
JSONRPCMessage::Response(_) => {
anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}");
JSONRPCMessage::ResultResponse(_) => {
anyhow::bail!("unexpected JSONRPCMessage::ResultResponse: {message:?}");
}
}
}

View File

@@ -13,10 +13,10 @@ use codex_mcp_server::ExecApprovalResponse;
use codex_mcp_server::PatchApprovalElicitRequestParams;
use codex_mcp_server::PatchApprovalResponse;
use mcp_types::ElicitRequest;
use mcp_types::ElicitRequestParamsRequestedSchema;
use mcp_types::ElicitRequestFormParamsRequestedSchema;
use mcp_types::JSONRPC_VERSION;
use mcp_types::JSONRPCRequest;
use mcp_types::JSONRPCResponse;
use mcp_types::JSONRPCResultResponse;
use mcp_types::ModelContextProtocolRequest;
use mcp_types::RequestId;
use pretty_assertions::assert_eq;
@@ -150,7 +150,7 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> {
)
.await??;
assert_eq!(
JSONRPCResponse {
JSONRPCResultResponse {
jsonrpc: JSONRPC_VERSION.into(),
id: RequestId::Integer(codex_request_id),
result: json!({
@@ -194,7 +194,8 @@ fn create_expected_elicitation_request(
method: ElicitRequest::METHOD.to_string(),
params: Some(serde_json::to_value(&ExecApprovalElicitRequestParams {
message: expected_message,
requested_schema: ElicitRequestParamsRequestedSchema {
requested_schema: ElicitRequestFormParamsRequestedSchema {
schema: None,
r#type: "object".to_string(),
properties: json!({}),
required: None,
@@ -312,7 +313,7 @@ async fn patch_approval_triggers_elicitation() -> anyhow::Result<()> {
)
.await??;
assert_eq!(
JSONRPCResponse {
JSONRPCResultResponse {
jsonrpc: JSONRPC_VERSION.into(),
id: RequestId::Integer(codex_request_id),
result: json!({
@@ -451,7 +452,8 @@ fn create_expected_patch_approval_elicitation_request(
method: ElicitRequest::METHOD.to_string(),
params: Some(serde_json::to_value(&PatchApprovalElicitRequestParams {
message: message_lines.join("\n"),
requested_schema: ElicitRequestParamsRequestedSchema {
requested_schema: ElicitRequestFormParamsRequestedSchema {
schema: None,
r#type: "object".to_string(),
properties: json!({}),
required: None,

View File

@@ -18,7 +18,7 @@ from shutil import copy2
from typing import Any, Literal
SCHEMA_VERSION = "2025-06-18"
SCHEMA_VERSION = "2025-11-25"
JSONRPC_VERSION = "2.0"
STANDARD_DERIVE = "#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]\n"
@@ -26,7 +26,8 @@ STANDARD_HASHABLE_DERIVE = (
"#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq, JsonSchema, TS)]\n"
)
# Will be populated with the schema's `definitions` map in `main()` so that
# Will be populated with the schema's definitions map (`definitions` in
# draft-07, `$defs` in 2020-12) in `main()` so that
# helper functions (for example `define_any_of`) can perform look-ups while
# generating code.
DEFINITIONS: dict[str, Any] = {}
@@ -85,7 +86,7 @@ def generate_lib_rs(schema_file: Path, lib_rs: Path, fmt: bool) -> None:
with schema_file.open(encoding="utf-8") as f:
schema_json = json.load(f)
DEFINITIONS = schema_json["definitions"]
DEFINITIONS = get_schema_definitions(schema_json)
out = [
f"""
@@ -124,7 +125,7 @@ fn default_jsonrpc() -> String {{ JSONRPC_VERSION.to_owned() }}
"""
]
definitions = schema_json["definitions"]
definitions = get_schema_definitions(schema_json)
# Keep track of every *Request type so we can generate the TryFrom impl at
# the end.
# The concrete *Request types referenced by the ClientRequest enum will be
@@ -283,13 +284,25 @@ def add_definition(name: str, definition: dict[str, Any], out: list[str]) -> Non
# Special carve-out for Result types:
if name.endswith("Result"):
out.extend(f"impl From<{name}> for serde_json::Value {{\n")
out.append(f" fn from(value: {name}) -> Self {{\n")
out.append(" // Leave this as it should never fail\n")
out.append(" #[expect(clippy::unwrap_used)]\n")
out.append(" serde_json::to_value(value).unwrap()\n")
out.append(" }\n")
out.append("}\n\n")
append_result_value_impl(name, out)
return
all_of = definition.get("allOf", [])
if all_of:
assert isinstance(all_of, list)
merged_properties: dict[str, Any] = {}
merged_required: set[str] = set()
for item in all_of:
if "$ref" in item:
ref_name = type_from_ref(item["$ref"])
item = DEFINITIONS[ref_name]
merged_properties.update(item.get("properties", {}))
merged_required.update(item.get("required", []))
out.extend(define_struct(name, merged_properties, merged_required, description))
if name.endswith("Result"):
append_result_value_impl(name, out)
return
enum_values = definition.get("enum", [])
@@ -333,6 +346,16 @@ def add_definition(name: str, definition: dict[str, Any], out: list[str]) -> Non
extra_defs = []
def append_result_value_impl(name: str, out: list[str]) -> None:
out.extend(f"impl From<{name}> for serde_json::Value {{\n")
out.append(f" fn from(value: {name}) -> Self {{\n")
out.append(" // Leave this as it should never fail\n")
out.append(" #[expect(clippy::unwrap_used)]\n")
out.append(" serde_json::to_value(value).unwrap()\n")
out.append(" }\n")
out.append("}\n\n")
@dataclass
class StructField:
viz: Literal["pub"] | Literal["const"]
@@ -384,10 +407,7 @@ def define_struct(
fields: list[StructField] = []
for prop_name, prop in properties.items():
if prop_name == "_meta":
# TODO?
continue
elif prop_name == "jsonrpc":
if prop_name == "jsonrpc":
fields.append(
StructField(
"pub",
@@ -400,6 +420,10 @@ def define_struct(
prop_type = map_type(prop, prop_name, name)
is_optional = prop_name not in required_props
if is_optional and prop_type.startswith("&'static str"):
# Optional `const` schema properties cannot be represented as a Rust
# associated const, so store them as optional strings.
prop_type = "String"
if is_optional:
prop_type = f"Option<{prop_type}>"
rs_prop = rust_prop_name(prop_name, is_optional)
@@ -539,8 +563,7 @@ def define_any_of(name: str, list_of_refs: list[Any], description: str | None =
full `…Request` struct from the schema definition.
"""
# Verify each item in list_of_refs is a dict with a $ref key.
refs = [item["$ref"] for item in list_of_refs if isinstance(item, dict)]
refs = [item["$ref"] for item in list_of_refs if isinstance(item, dict) and "$ref" in item]
out: list[str] = []
if description:
@@ -555,54 +578,67 @@ def define_any_of(name: str, list_of_refs: list[Any], description: str | None =
out.append(f"pub enum {name} {{\n")
if name == "ClientRequest":
if len(refs) != len(list_of_refs):
raise ValueError("ClientRequest anyOf must contain only $ref items.")
# Record the set of request type names so we can later generate a
# `TryFrom<JSONRPCRequest>` implementation.
global CLIENT_REQUEST_TYPE_NAMES
CLIENT_REQUEST_TYPE_NAMES = [type_from_ref(r) for r in refs]
if name == "ServerNotification":
if len(refs) != len(list_of_refs):
raise ValueError("ServerNotification anyOf must contain only $ref items.")
global SERVER_NOTIFICATION_TYPE_NAMES
SERVER_NOTIFICATION_TYPE_NAMES = [type_from_ref(r) for r in refs]
for ref in refs:
ref_name = type_from_ref(ref)
for idx, item in enumerate(list_of_refs):
if not isinstance(item, dict):
raise ValueError(f"Unexpected anyOf item for {name}: {item}")
# For JSONRPCMessage variants, drop the common "JSONRPC" prefix to
# make the enum easier to read (e.g. `Request` instead of
# `JSONRPCRequest`). The payload type remains unchanged.
variant_name = (
ref_name[len("JSONRPC") :]
if name == "JSONRPCMessage" and ref_name.startswith("JSONRPC")
else ref_name
)
ref = item.get("$ref")
if ref:
ref_name = type_from_ref(ref)
# Special-case for `ClientRequest` and `ServerNotification` so the enum
# variant's payload is the *Params type rather than the full *Request /
# *Notification marker type.
if name in ("ClientRequest", "ServerNotification"):
# Rely on the trait implementation to tell us the exact Rust type
# of the `params` payload. This guarantees we stay in sync with any
# special-case logic used elsewhere (e.g. objects with
# `additionalProperties` mapping to `serde_json::Value`).
if name == "ClientRequest":
payload_type = f"<{ref_name} as ModelContextProtocolRequest>::Params"
else:
payload_type = f"<{ref_name} as ModelContextProtocolNotification>::Params"
# Determine the wire value for `method` so we can annotate the
# variant appropriately. If for some reason the schema does not
# specify a constant we fall back to the type name, which will at
# least compile (although deserialization will likely fail).
request_def = DEFINITIONS.get(ref_name, {})
method_const = (
request_def.get("properties", {}).get("method", {}).get("const", ref_name)
# For JSONRPCMessage variants, drop the common "JSONRPC" prefix to
# make the enum easier to read (e.g. `Request` instead of
# `JSONRPCRequest`). The payload type remains unchanged.
variant_name = (
ref_name[len("JSONRPC") :]
if name == "JSONRPCMessage" and ref_name.startswith("JSONRPC")
else ref_name
)
out.append(f' #[serde(rename = "{method_const}")]\n')
out.append(f" {variant_name}({payload_type}),\n")
else:
# The regular/straight-forward case.
out.append(f" {variant_name}({ref_name}),\n")
# Special-case for `ClientRequest` and `ServerNotification` so the enum
# variant's payload is the *Params type rather than the full *Request /
# *Notification marker type.
if name in ("ClientRequest", "ServerNotification"):
# Rely on the trait implementation to tell us the exact Rust type
# of the `params` payload. This guarantees we stay in sync with any
# special-case logic used elsewhere (e.g. objects with
# `additionalProperties` mapping to `serde_json::Value`).
if name == "ClientRequest":
payload_type = f"<{ref_name} as ModelContextProtocolRequest>::Params"
else:
payload_type = f"<{ref_name} as ModelContextProtocolNotification>::Params"
# Determine the wire value for `method` so we can annotate the
# variant appropriately. If for some reason the schema does not
# specify a constant we fall back to the type name, which will at
# least compile (although deserialization will likely fail).
request_def = DEFINITIONS.get(ref_name, {})
method_const = (
request_def.get("properties", {}).get("method", {}).get("const", ref_name)
)
out.append(f' #[serde(rename = "{method_const}")]\n')
out.append(f" {variant_name}({payload_type}),\n")
else:
# The regular/straight-forward case.
out.append(f" {variant_name}({ref_name}),\n")
continue
inline_type = map_type(item, f"variant_{idx}", name)
out.append(f" Variant{idx}({inline_type}),\n")
out.append("}\n\n")
return out
@@ -704,6 +740,11 @@ def rust_prop_name(name: str, is_optional: bool) -> RustProp:
is_rename = False
if name == "type":
prop_name = "r#type"
elif name == "$schema":
prop_name = "schema"
is_rename = True
elif name == "const":
prop_name = "r#const"
elif name == "ref":
prop_name = "r#ref"
elif name == "enum":
@@ -758,9 +799,20 @@ def check_string_list(value: Any) -> list[str] | None:
return value
def get_schema_definitions(schema_json: dict[str, Any]) -> dict[str, Any]:
"""Return the schema definitions map across supported JSON Schema drafts."""
definitions = schema_json.get("definitions")
if definitions is None:
definitions = schema_json.get("$defs")
if isinstance(definitions, dict):
return definitions
raise KeyError("Schema is missing a definitions map (`definitions` or `$defs`).")
def type_from_ref(ref: str) -> str:
"""Convert a JSON reference to a Rust type."""
assert ref.startswith("#/definitions/")
if not (ref.startswith("#/definitions/") or ref.startswith("#/$defs/")):
raise ValueError(f"Unsupported $ref format: {ref}")
return ref.split("/")[-1]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -52,16 +52,21 @@ fn deserialize_initialize_request() {
assert_eq!(
init_params,
InitializeRequestParams {
_meta: None,
capabilities: ClientCapabilities {
experimental: None,
roots: None,
sampling: None,
elicitation: None,
tasks: None,
},
client_info: Implementation {
description: None,
icons: None,
name: "acme-client".into(),
title: Some("Acme".to_string()),
version: "1.2.3".into(),
website_url: None,
user_agent: None,
},
protocol_version: "2025-06-18".into(),

View File

@@ -33,6 +33,7 @@ fn deserialize_progress_notification() {
};
let expected_params = ProgressNotificationParams {
_meta: None,
message: Some("Half way there".into()),
progress: 0.5,
progress_token: ProgressToken::Integer(99),

View File

@@ -805,6 +805,7 @@ impl From<&CallToolResult> for FunctionCallOutputPayload {
content,
structured_content,
is_error,
..
} = call_tool_result;
let is_success = is_error != &Some(true);
@@ -1039,13 +1040,16 @@ mod tests {
#[test]
fn serializes_image_outputs_as_array() -> Result<()> {
let call_tool_result = CallToolResult {
_meta: None,
content: vec![
ContentBlock::TextContent(TextContent {
_meta: None,
annotations: None,
text: "caption".into(),
r#type: "text".into(),
}),
ContentBlock::ImageContent(ImageContent {
_meta: None,
annotations: None,
data: "BASE64".into(),
mime_type: "image/png".into(),

View File

@@ -14,12 +14,10 @@ use mcp_types::CallToolRequestParams;
use mcp_types::CallToolResult;
use mcp_types::InitializeRequestParams;
use mcp_types::InitializeResult;
use mcp_types::ListResourceTemplatesRequestParams;
use mcp_types::ListResourceTemplatesResult;
use mcp_types::ListResourcesRequestParams;
use mcp_types::ListResourcesResult;
use mcp_types::ListToolsRequestParams;
use mcp_types::ListToolsResult;
use mcp_types::PaginatedRequestParams;
use mcp_types::ReadResourceRequestParams;
use mcp_types::ReadResourceResult;
use mcp_types::RequestId;
@@ -296,11 +294,12 @@ impl RmcpClient {
pub async fn list_tools(
&self,
params: Option<ListToolsRequestParams>,
params: Option<PaginatedRequestParams>,
timeout: Option<Duration>,
) -> Result<ListToolsResult> {
let result = self.list_tools_with_connector_ids(params, timeout).await?;
Ok(ListToolsResult {
_meta: None,
next_cursor: result.next_cursor,
tools: result.tools.into_iter().map(|tool| tool.tool).collect(),
})
@@ -308,7 +307,7 @@ impl RmcpClient {
pub async fn list_tools_with_connector_ids(
&self,
params: Option<ListToolsRequestParams>,
params: Option<PaginatedRequestParams>,
timeout: Option<Duration>,
) -> Result<ListToolsWithConnectorIdResult> {
self.refresh_oauth_if_needed().await;
@@ -352,7 +351,7 @@ impl RmcpClient {
pub async fn list_resources(
&self,
params: Option<ListResourcesRequestParams>,
params: Option<PaginatedRequestParams>,
timeout: Option<Duration>,
) -> Result<ListResourcesResult> {
self.refresh_oauth_if_needed().await;
@@ -370,7 +369,7 @@ impl RmcpClient {
pub async fn list_resource_templates(
&self,
params: Option<ListResourceTemplatesRequestParams>,
params: Option<PaginatedRequestParams>,
timeout: Option<Duration>,
) -> Result<ListResourceTemplatesResult> {
self.refresh_oauth_if_needed().await;
@@ -409,7 +408,12 @@ impl RmcpClient {
) -> Result<CallToolResult> {
self.refresh_oauth_if_needed().await;
let service = self.service().await?;
let params = CallToolRequestParams { arguments, name };
let params = CallToolRequestParams {
_meta: None,
arguments,
name,
task: None,
};
let rmcp_params: CallToolRequestParam = convert_to_rmcp(params)?;
let fut = service.call_tool(rmcp_params);
let rmcp_result = run_with_timeout(fut, timeout, "tools/call").await?;

View File

@@ -8,6 +8,7 @@ use codex_rmcp_client::RmcpClient;
use codex_utils_cargo_bin::CargoBinError;
use futures::FutureExt as _;
use mcp_types::ClientCapabilities;
use mcp_types::ClientCapabilitiesElicitation;
use mcp_types::Implementation;
use mcp_types::InitializeRequestParams;
use mcp_types::ListResourceTemplatesResult;
@@ -26,16 +27,24 @@ fn stdio_server_bin() -> Result<PathBuf, CargoBinError> {
fn init_params() -> InitializeRequestParams {
InitializeRequestParams {
_meta: None,
capabilities: ClientCapabilities {
elicitation: Some(ClientCapabilitiesElicitation {
form: None,
url: None,
}),
experimental: None,
roots: None,
sampling: None,
elicitation: Some(json!({})),
tasks: None,
},
client_info: Implementation {
description: None,
icons: None,
name: "codex-test".into(),
version: "0.0.0-test".into(),
title: Some("Codex rmcp resource test".into()),
website_url: None,
user_agent: None,
},
protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_string(),
@@ -80,8 +89,10 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> {
assert_eq!(
memo,
&Resource {
_meta: None,
annotations: None,
description: Some("A sample MCP resource exposed for integration tests.".to_string()),
icons: None,
mime_type: Some("text/plain".to_string()),
name: "example-note".to_string(),
size: None,
@@ -95,12 +106,15 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> {
assert_eq!(
templates,
ListResourceTemplatesResult {
_meta: None,
next_cursor: None,
resource_templates: vec![ResourceTemplate {
_meta: None,
annotations: None,
description: Some(
"Template for memo://codex/{slug} resources used in tests.".to_string()
),
icons: None,
mime_type: Some("text/plain".to_string()),
name: "codex-memo".to_string(),
title: Some("Codex Memo".to_string()),
@@ -112,6 +126,7 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> {
let read = client
.read_resource(
ReadResourceRequestParams {
_meta: None,
uri: RESOURCE_URI.to_string(),
},
Some(Duration::from_secs(5)),
@@ -125,6 +140,7 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> {
assert_eq!(
text,
&TextResourceContents {
_meta: None,
text: "This is a sample MCP resource served by the rmcp test server.".to_string(),
uri: RESOURCE_URI.to_string(),
mime_type: Some("text/plain".to_string()),