fix(mcp): tolerate numeric protocolVersion in initialize

This commit is contained in:
James Peter
2025-09-22 16:16:05 +10:00
parent 3cbf785eb9
commit 763e08c56d
5 changed files with 284 additions and 57 deletions
+40 -1
View File
@@ -102,7 +102,7 @@ pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
Err(e) => MaybeApplyPatch::PatchParseError(e),
},
[bash, flag, script]
if bash == "bash"
if is_bash_like(bash)
&& flag == "-lc"
&& script.trim_start().starts_with("apply_patch") =>
{
@@ -118,6 +118,17 @@ pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
}
}
fn is_bash_like(cmd: &str) -> bool {
let trimmed = cmd.trim_matches('"').trim_matches('\'');
let basename = Path::new(trimmed)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(trimmed)
.to_ascii_lowercase();
matches!(basename.as_str(), "bash" | "bash.exe")
}
#[derive(Debug, PartialEq)]
pub enum ApplyPatchFileChange {
Add {
@@ -743,6 +754,34 @@ PATCH"#,
}
}
#[test]
fn test_heredoc_with_absolute_bash() {
let script = [
"apply_patch <<'PATCH'",
"*** Begin Patch",
"*** Add File: foo",
"+hi",
"*** End Patch",
"PATCH",
]
.join("\n");
let args = strs_to_strings(&["/bin/bash", "-lc", &script]);
match maybe_parse_apply_patch(&args) {
MaybeApplyPatch::Body(hunks) => {
assert_eq!(
hunks,
vec![Hunk::AddFile {
path: PathBuf::from("foo"),
contents: "hi\n".to_string()
}]
);
}
result => panic!("expected MaybeApplyPatch::Body got {result:?}"),
}
}
#[tokio::test]
async fn test_add_file_hunk_creates_file_with_contents() {
let dir = tempdir().unwrap();
+81 -23
View File
@@ -36,7 +36,6 @@ use mcp_types::JSONRPCResponse;
use mcp_types::ListToolsResult;
use mcp_types::ModelContextProtocolRequest;
use mcp_types::RequestId;
use mcp_types::ServerCapabilitiesTools;
use mcp_types::ServerNotification;
use mcp_types::TextContent;
use serde_json::json;
@@ -103,6 +102,50 @@ impl MessageProcessor {
// Hold on to the ID so we can respond.
let request_id = request.id.clone();
let mut request = request;
if request.method == mcp_types::InitializeRequest::METHOD {
if let Some(params) = request.params.as_mut() {
if let Some(protocol_version) = params.get_mut("protocolVersion") {
if let Some(num) = protocol_version.as_i64() {
*protocol_version = serde_json::Value::String(num.to_string());
} else if let Some(num) = protocol_version.as_u64() {
*protocol_version = serde_json::Value::String(num.to_string());
} else if protocol_version.is_null() {
*protocol_version = serde_json::Value::String("1".to_string());
}
}
if let serde_json::Value::Object(map) = params {
if !map.contains_key("capabilities") {
let capabilities = map
.remove("clientCapabilities")
.unwrap_or_else(|| serde_json::Value::Object(Default::default()));
let mut cap_wrapper = serde_json::Map::new();
cap_wrapper.insert("experimental".to_string(), capabilities);
map.insert(
"capabilities".to_string(),
serde_json::Value::Object(cap_wrapper),
);
}
map.entry("clientInfo").or_insert_with(|| {
let mut info = serde_json::Map::new();
info.insert(
"name".to_string(),
serde_json::Value::String("unknown-client".into()),
);
info.insert(
"version".to_string(),
serde_json::Value::String("0.0.0".into()),
);
serde_json::Value::Object(info)
});
}
}
}
let client_request = match McpClientRequest::try_from(request) {
Ok(client_request) => client_request,
Err(e) => {
@@ -233,29 +276,44 @@ impl MessageProcessor {
self.initialized = true;
// Build a minimal InitializeResult. Fill with placeholders.
let result = mcp_types::InitializeResult {
capabilities: mcp_types::ServerCapabilities {
completions: None,
experimental: None,
logging: None,
prompts: None,
resources: None,
tools: Some(ServerCapabilitiesTools {
list_changed: Some(true),
}),
},
instructions: None,
protocol_version: params.protocol_version.clone(),
server_info: mcp_types::Implementation {
name: "code-mcp-server".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
title: Some("Codex".to_string()),
user_agent: Some(get_codex_user_agent_default()),
},
};
let server_info = serde_json::json!({
"name": "code-mcp-server",
"version": env!("CARGO_PKG_VERSION"),
"title": "Codex",
"user_agent": get_codex_user_agent_default(),
});
self.send_response::<mcp_types::InitializeRequest>(id, result)
.await;
let agent_capabilities = serde_json::json!({
"promptCapabilities": {
"image": true,
"embeddedContext": true,
"audio": false
},
"mcpCapabilities": {
"http": false,
"sse": false
}
});
let auth_methods = serde_json::json!([{
"id": "code-login",
"name": "Use Code login",
"description": "Run `code login` (ChatGPT or API key) before connecting."
}]);
let result = serde_json::json!({
"protocolVersion": 1,
"serverInfo": server_info,
"capabilities": {
"tools": {
"listChanged": true
}
},
"agentCapabilities": agent_capabilities,
"authMethods": auth_methods
});
self.outgoing.send_response(id, result).await;
}
async fn send_response<T>(&self, id: RequestId, result: T::Result)
+5
View File
@@ -5227,12 +5227,17 @@ impl ChatWidget<'_> {
} else {
None
};
let context_window = self.config.model_context_window;
let context_tokens_used = context_window.map(|_| self.last_token_usage.tokens_in_context_window());
RateLimitResetInfo {
primary_last_reset: self.rate_limit_last_primary_reset_at,
weekly_last_reset: self.rate_limit_last_weekly_reset_at,
session_tokens_used,
auto_compact_limit,
overflow_auto_compact: true,
context_window,
context_tokens_used,
}
}
+30 -4
View File
@@ -9687,10 +9687,8 @@ pub(crate) fn new_status_output(
)));
}
if let Some(limit) = auto_compact_limit {
if limit <= 0 {
lines.push(Line::from(" • Auto-compact threshold: disabled"));
} else {
match auto_compact_limit {
Some(limit) if limit > 0 => {
let limit_u64 = limit as u64;
let remaining = limit_u64.saturating_sub(total_usage.total_tokens);
lines.push(Line::from(format!(
@@ -9702,6 +9700,34 @@ pub(crate) fn new_status_output(
lines.push(Line::from(" • Compacting will trigger on the next turn".dim()));
}
}
_ => {
if let Some(window) = context_window {
if window > 0 {
let used = last_usage.tokens_in_context_window();
let remaining = window.saturating_sub(used);
let percent_left = if window == 0 {
0.0
} else {
(remaining as f64 / window as f64) * 100.0
};
lines.push(Line::from(format!(
" • Context window: {} used of {} ({:.0}% left)",
format_with_separators(used),
format_with_separators(window),
percent_left
)));
lines.push(Line::from(format!(
" • {} tokens before overflow",
format_with_separators(remaining)
)));
lines.push(Line::from(" • Auto-compaction runs after overflow errors".to_string()));
} else {
lines.push(Line::from(" • Auto-compaction runs after overflow errors".to_string()));
}
} else {
lines.push(Line::from(" • Auto-compaction runs after overflow errors".to_string()));
}
}
}
}
+128 -29
View File
@@ -5,6 +5,7 @@ use codex_core::protocol::RateLimitSnapshotEvent;
use codex_protocol::num_format::format_with_separators;
use ratatui::prelude::*;
use ratatui::style::Stylize;
use std::time::Duration;
const WEEKLY_CELL: &str = "▇▇";
const HOURLY_CELL: &str = "▓▓";
@@ -50,6 +51,9 @@ pub(crate) struct RateLimitResetInfo {
pub(crate) weekly_last_reset: Option<DateTime<Utc>>,
pub(crate) session_tokens_used: Option<u64>,
pub(crate) auto_compact_limit: Option<u64>,
pub(crate) overflow_auto_compact: bool,
pub(crate) context_window: Option<u64>,
pub(crate) context_tokens_used: Option<u64>,
}
/// Default gauge configuration used by the TUI.
@@ -69,7 +73,7 @@ pub(crate) fn build_limits_view(
.map(|state| scale_grid_state(state, grid_config));
LimitsView {
summary_lines: build_summary_lines(&metrics, snapshot, reset_info),
summary_lines: build_summary_lines(&metrics, snapshot, &reset_info),
legend_lines: build_legend_lines(grid_state.is_some()),
footer_lines: build_footer_lines(&metrics),
grid_state,
@@ -126,7 +130,7 @@ struct GridState {
fn build_summary_lines(
metrics: &RateLimitMetrics,
snapshot: &RateLimitSnapshotEvent,
reset_info: RateLimitResetInfo,
reset_info: &RateLimitResetInfo,
) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push("/limits".magenta().into());
@@ -141,7 +145,10 @@ fn build_summary_lines(
.fg(colors::text())
.add_modifier(Modifier::BOLD),
));
lines.push(build_hourly_window_line(metrics));
lines.push(build_hourly_window_line(
metrics,
reset_info.primary_last_reset,
));
lines.push(build_hourly_reset_line(
snapshot.primary_window_minutes,
reset_info.primary_last_reset,
@@ -168,7 +175,7 @@ fn build_summary_lines(
));
lines.push("".into());
lines.extend(build_compact_lines(&reset_info));
lines.extend(build_compact_lines(reset_info));
lines.push("".into());
lines.push(section_header("Chart"));
@@ -199,28 +206,52 @@ fn build_bar_line(label: &str, percent: f64, suffix: &str, style: Style) -> Line
Line::from(spans)
}
fn build_hourly_window_line(metrics: &RateLimitMetrics) -> Line<'static> {
let mut spans: Vec<Span<'static>> = Vec::new();
spans.push(Span::raw(field_prefix("Window")));
let ratio = metrics
.primary_to_weekly_ratio_percent
.unwrap_or(0.0)
.clamp(0.0, 100.0);
spans.extend(render_percent_bar(ratio));
let primary = format_minutes_short(metrics.primary_window_minutes);
let weekly = format_minutes_short(metrics.weekly_window_minutes);
if let Some(value) = metrics.primary_to_weekly_ratio_percent {
spans.push(Span::styled(
format!(" {}", format_percent(value)),
Style::default().fg(colors::text()),
));
fn build_hourly_window_line(
metrics: &RateLimitMetrics,
last_reset: Option<DateTime<Utc>>,
) -> Line<'static> {
let prefix = field_prefix("Window");
if metrics.primary_window_minutes == 0 {
return Line::from(vec![
Span::raw(prefix),
Span::styled(
"window length unavailable".to_string(),
Style::default().fg(colors::dim()),
),
]);
}
spans.push(Span::styled(
format!(" ({primary} / {weekly})"),
Style::default().fg(colors::dim()),
));
Line::from(spans)
if let Some(last) = last_reset {
if let Some(timing) = compute_window_timing(metrics.primary_window_minutes, last) {
let window_secs = timing.window.as_secs_f64();
if window_secs > 0.0 {
let elapsed = timing.elapsed();
let percent = ((elapsed.as_secs_f64() / window_secs) * 100.0).clamp(0.0, 100.0);
let mut spans: Vec<Span<'static>> = Vec::new();
spans.push(Span::raw(prefix.clone()));
spans.extend(render_percent_bar(percent));
spans.push(Span::styled(
format!(" {}", format_percent(percent)),
Style::default().fg(colors::text()),
));
let elapsed_display = format_duration(elapsed);
let total_display = format_minutes_short(metrics.primary_window_minutes);
spans.push(Span::styled(
format!(" ({elapsed_display} / {total_display})"),
Style::default().fg(colors::dim()),
));
return Line::from(spans);
}
}
}
Line::from(vec![
Span::raw(prefix),
Span::styled(
format!("≈{} rolling window", format_minutes_short(metrics.primary_window_minutes)),
Style::default().fg(colors::dim()),
),
])
}
fn build_hourly_reset_line(
@@ -305,10 +336,23 @@ fn build_compact_lines(reset_info: &RateLimitResetInfo) -> Vec<Line<'static>> {
}
}
None => {
lines.push(Line::from(vec![Span::styled(
format!("{FIELD_INDENT}Auto-compact threshold disabled"),
Style::default().fg(colors::dim()),
)]));
if let (Some(window), Some(used)) =
(reset_info.context_window, reset_info.context_tokens_used)
{
lines.push(build_context_tokens_line(used, window));
lines.push(build_context_remaining_line(used, window));
lines.push(build_context_status_line(reset_info.overflow_auto_compact));
} else if reset_info.overflow_auto_compact {
lines.push(Line::from(vec![Span::styled(
format!("{FIELD_INDENT}Auto-compaction runs after overflow errors"),
Style::default().fg(colors::dim()),
)]));
} else {
lines.push(Line::from(vec![Span::styled(
format!("{FIELD_INDENT}Auto-compaction unavailable"),
Style::default().fg(colors::dim()),
)]));
}
}
}
@@ -380,6 +424,61 @@ fn build_compact_status_line(used: u64, limit: u64) -> Line<'static> {
])
}
fn build_context_tokens_line(used: u64, window: u64) -> Line<'static> {
let percent = if window == 0 {
0.0
} else {
(used as f64 / window as f64) * 100.0
};
let mut spans: Vec<Span<'static>> = Vec::new();
spans.push(Span::raw(field_prefix("Context")));
spans.extend(render_percent_bar(percent));
let percent_display = if percent > 100.0 {
format!("{percent:.0}%")
} else {
format_percent(percent)
};
spans.push(Span::styled(
format!(" {percent_display}"),
Style::default().fg(colors::text()),
));
spans.push(Span::styled(
format!(
" ({} / {})",
format_with_separators(used),
format_with_separators(window)
),
Style::default().fg(colors::dim()),
));
Line::from(spans)
}
fn build_context_remaining_line(used: u64, window: u64) -> Line<'static> {
let remaining = window.saturating_sub(used);
Line::from(vec![
Span::raw(field_prefix("Remaining")),
Span::styled(
format!(
"{} tokens before overflow",
format_with_separators(remaining)
),
Style::default().fg(colors::dim()),
),
])
}
fn build_context_status_line(overflow_auto_compact: bool) -> Line<'static> {
let status = if overflow_auto_compact {
"Auto-compaction runs after overflow errors"
} else {
"Auto-compaction unavailable"
};
Line::from(vec![
Span::raw(field_prefix("Status")),
Span::styled(status.to_string(), Style::default().fg(colors::dim())),
])
}
fn compute_reset_eta(
window_minutes: u64,
last_reset: DateTime<Utc>,