mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(mcp): tolerate numeric protocolVersion in initialize
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user