mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
1 Commits
main
...
dev/mzeng/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90e9ad5ffb |
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")],
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
4055
codex-rs/mcp-types/schema/2025-11-25/schema.json
Normal file
4055
codex-rs/mcp-types/schema/2025-11-25/schema.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user