mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
## What changed - Drive multi-round `tools/call` and `resources/read` requests through `input_required` responses, preserving opaque request state and elicitation metadata across JSON, SSE, and stdio transports. - Decode modern discovery and elicitation result shapes while retaining legacy initialization, elicitation defaults, and discovery fallback behavior. - Apply the 8 MiB modern-protocol response limit to JSON, SSE events, and stdio messages, and reject executor stream recovery when output sequence gaps would corrupt the protocol stream. ## Testing Added integration coverage for discovery, multi-round requests, legacy fallbacks, message limits, stdio launchers, SSE responses, session recovery, and executor output recovery. GitOrigin-RevId: d26a08883a2825e8ce2408ac498114b33859225e
251 lines
9.7 KiB
Rust
251 lines
9.7 KiB
Rust
use std::io;
|
|
use std::io::BufRead;
|
|
use std::io::Write;
|
|
|
|
use anyhow::Result;
|
|
use serde_json::Value;
|
|
use serde_json::json;
|
|
|
|
const MODERN_VERSION: &str = "2026-07-28";
|
|
const LEGACY_VERSION: &str = "2025-06-18";
|
|
const MAX_MESSAGE_BYTES: usize = 8 * 1024 * 1024;
|
|
|
|
fn write_message(stdout: &mut io::StdoutLock<'_>, message: Value) -> Result<()> {
|
|
serde_json::to_writer(&mut *stdout, &message)?;
|
|
stdout.write_all(b"\n")?;
|
|
stdout.flush()?;
|
|
Ok(())
|
|
}
|
|
|
|
fn assert_modern_metadata(message: &Value) {
|
|
let metadata = &message["params"]["_meta"];
|
|
assert_eq!(
|
|
metadata["io.modelcontextprotocol/protocolVersion"],
|
|
MODERN_VERSION
|
|
);
|
|
assert!(metadata["io.modelcontextprotocol/clientInfo"].is_object());
|
|
assert!(metadata["io.modelcontextprotocol/clientCapabilities"].is_object());
|
|
}
|
|
|
|
fn legacy_schema_defaults() -> Value {
|
|
json!({
|
|
"name": "John Doe",
|
|
"age": 30,
|
|
"score": 95.5,
|
|
"status": "active",
|
|
"verified": true,
|
|
})
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let mode = std::env::args().nth(1).unwrap_or_default();
|
|
assert!(matches!(
|
|
mode.as_str(),
|
|
"modern" | "legacy" | "legacy-fallback" | "oversized-stdout" | "oversized-stdout-legacy"
|
|
));
|
|
assert!(std::env::var_os("CODEX_MCP_PROTOCOL_VERSION").is_none());
|
|
let is_legacy = mode.starts_with("legacy") || mode.ends_with("-legacy");
|
|
let oversized = mode.starts_with("oversized-stdout");
|
|
let mut stdout = io::stdout().lock();
|
|
let mut initialized = false;
|
|
let mut pending_legacy_call = None;
|
|
|
|
for line in io::stdin().lock().lines() {
|
|
let message: Value = serde_json::from_str(&line?)?;
|
|
let Some(method) = message.get("method").and_then(Value::as_str) else {
|
|
let pending_call = pending_legacy_call
|
|
.take()
|
|
.ok_or_else(|| anyhow::anyhow!("unexpected server-request response"))?;
|
|
assert_eq!(message["id"], "legacy-approval");
|
|
assert_eq!(message["result"]["action"], "accept");
|
|
assert_eq!(message["result"]["content"], legacy_schema_defaults());
|
|
write_message(
|
|
&mut stdout,
|
|
json!({
|
|
"jsonrpc": "2.0",
|
|
"id": pending_call,
|
|
"result": {"content": [{"type": "text", "text": "legacy approved"}]},
|
|
}),
|
|
)?;
|
|
continue;
|
|
};
|
|
|
|
match method {
|
|
"server/discover" if is_legacy => {
|
|
assert_eq!(mode, "legacy-fallback");
|
|
write_message(
|
|
&mut stdout,
|
|
json!({
|
|
"jsonrpc": "2.0",
|
|
"id": message["id"],
|
|
"error": {"code": -32601, "message": "method not found"},
|
|
}),
|
|
)?;
|
|
}
|
|
"server/discover" => {
|
|
assert!(!initialized);
|
|
assert_modern_metadata(&message);
|
|
initialized = true;
|
|
write_message(
|
|
&mut stdout,
|
|
json!({
|
|
"jsonrpc": "2.0",
|
|
"id": message["id"],
|
|
"result": {
|
|
"resultType": "complete",
|
|
"supportedVersions": [MODERN_VERSION],
|
|
"capabilities": {"tools": {}, "resources": {}},
|
|
"_meta": {
|
|
"io.modelcontextprotocol/serverInfo": {
|
|
"name": "strict-stdio-test",
|
|
"version": "1.0.0",
|
|
},
|
|
},
|
|
"ttlMs": 0,
|
|
"cacheScope": "private",
|
|
},
|
|
}),
|
|
)?;
|
|
}
|
|
"initialize" => {
|
|
assert!(is_legacy);
|
|
assert_eq!(message["params"]["protocolVersion"], LEGACY_VERSION);
|
|
initialized = true;
|
|
write_message(
|
|
&mut stdout,
|
|
json!({
|
|
"jsonrpc": "2.0",
|
|
"id": message["id"],
|
|
"result": {
|
|
"protocolVersion": LEGACY_VERSION,
|
|
"capabilities": {"tools": {}},
|
|
"serverInfo": {"name": "legacy-stdio-test", "version": "1.0.0"},
|
|
},
|
|
}),
|
|
)?;
|
|
}
|
|
"notifications/initialized" => assert!(is_legacy),
|
|
"tools/list" => {
|
|
assert!(initialized);
|
|
if !is_legacy {
|
|
assert_modern_metadata(&message);
|
|
}
|
|
let description = if oversized {
|
|
"x".repeat(MAX_MESSAGE_BYTES + 1)
|
|
} else {
|
|
"echo a value".to_owned()
|
|
};
|
|
let mut result = json!({
|
|
"tools": [{
|
|
"name": "echo",
|
|
"description": description,
|
|
"inputSchema": {"type": "object"},
|
|
}],
|
|
});
|
|
if !is_legacy {
|
|
result["resultType"] = json!("complete");
|
|
result["ttlMs"] = json!(0);
|
|
result["cacheScope"] = json!("private");
|
|
}
|
|
write_message(
|
|
&mut stdout,
|
|
json!({"jsonrpc": "2.0", "id": message["id"], "result": result}),
|
|
)?;
|
|
}
|
|
"tools/call" if is_legacy => {
|
|
assert!(initialized);
|
|
pending_legacy_call = Some(message["id"].clone());
|
|
write_message(
|
|
&mut stdout,
|
|
json!({
|
|
"jsonrpc": "2.0",
|
|
"id": "legacy-approval",
|
|
"method": "elicitation/create",
|
|
"params": {
|
|
"message": "Accept the schema defaults",
|
|
"requestedSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string", "default": "John Doe"},
|
|
"age": {"type": "integer", "default": 30},
|
|
"score": {"type": "number", "default": 95.5},
|
|
"status": {
|
|
"type": "string",
|
|
"enum": ["active", "inactive"],
|
|
"default": "active",
|
|
},
|
|
"verified": {"type": "boolean", "default": true},
|
|
},
|
|
"required": [],
|
|
},
|
|
},
|
|
}),
|
|
)?;
|
|
}
|
|
"tools/call" => {
|
|
assert!(initialized);
|
|
assert_modern_metadata(&message);
|
|
if message.pointer("/params/inputResponses").is_none() {
|
|
write_message(
|
|
&mut stdout,
|
|
json!({
|
|
"jsonrpc": "2.0",
|
|
"id": message["id"],
|
|
"result": {
|
|
"resultType": "input_required",
|
|
"inputRequests": {
|
|
"approval": {
|
|
"method": "elicitation/create",
|
|
"params": {
|
|
"mode": "form",
|
|
"message": "Approve stdio call?",
|
|
"requestedSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"approved": {"type": "boolean"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"requestState": "stdio-state",
|
|
"_meta": {
|
|
"io.modelcontextprotocol/serverInfo": {
|
|
"name": "strict-stdio-test",
|
|
"version": "1.0.0",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)?;
|
|
continue;
|
|
}
|
|
|
|
assert_eq!(message["params"]["requestState"], "stdio-state");
|
|
assert_eq!(
|
|
message["params"]["inputResponses"]["approval"]["action"],
|
|
"accept"
|
|
);
|
|
assert_eq!(
|
|
message["params"]["inputResponses"]["approval"]["content"]["approved"],
|
|
true
|
|
);
|
|
write_message(
|
|
&mut stdout,
|
|
json!({
|
|
"jsonrpc": "2.0",
|
|
"id": message["id"],
|
|
"result": {
|
|
"resultType": "complete",
|
|
"content": [{"type": "text", "text": "modern approved"}],
|
|
},
|
|
}),
|
|
)?;
|
|
}
|
|
other => anyhow::bail!("unexpected MCP method {other}"),
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|