Skip to content

Commit 05fccfe

Browse files
committed
fix(gemini): map http MCP entries to httpUrl in settings (codex P2)
Gemini CLI reads mcpServers 'url' as an SSE endpoint and 'httpUrl' as streamable HTTP; type=http entries now move their url to httpUrl so HTTP MCP servers connect instead of being treated as SSE.
1 parent fad5d6e commit 05fccfe

1 file changed

Lines changed: 48 additions & 4 deletions

File tree

animus-session-backend/src/session/gemini/transport.rs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,30 @@ pub(crate) fn gemini_invocation_for_request(
148148
Ok(invocation)
149149
}
150150

151+
/// Translate one canonical `mcp_servers` entry into the Gemini settings
152+
/// `mcpServers` shape. Gemini reads `url` as an SSE endpoint and `httpUrl`
153+
/// as streamable HTTP, so `{"type": "http"}` entries move their `url` to
154+
/// `httpUrl`; stdio and SSE entries pass through unchanged
155+
/// (`command`/`args`/`env`, `url` + `headers`).
156+
fn gemini_mcp_entry(entry: &serde_json::Value) -> serde_json::Value {
157+
let Some(object) = entry.as_object() else {
158+
return entry.clone();
159+
};
160+
if object.get("type").and_then(serde_json::Value::as_str) != Some("http") {
161+
return entry.clone();
162+
}
163+
let Some(url) = object.get("url").cloned() else {
164+
return entry.clone();
165+
};
166+
let mut translated = object.clone();
167+
translated.remove("url");
168+
translated.insert("httpUrl".to_string(), url);
169+
serde_json::Value::Object(translated)
170+
}
171+
151172
/// Build the settings JSON injected via `GEMINI_CLI_SYSTEM_SETTINGS_PATH`
152173
/// when the request carries MCP servers, or `None` when `mcp_servers` is
153-
/// absent or empty. The Gemini CLI settings `mcpServers` schema accepts
154-
/// the canonical entries unchanged (`command`/`args`/`env` for stdio,
155-
/// `type` + `url` + `headers` for remote), so they pass through as-is.
174+
/// absent or empty. Entries are translated via [`gemini_mcp_entry`].
156175
/// `existing` is the current system settings document (if any); its keys
157176
/// are preserved, with the per-run servers shallow-merged into
158177
/// `mcpServers` (per-run entries win on name conflicts).
@@ -171,7 +190,7 @@ fn gemini_settings_with_mcp_servers(
171190
.cloned()
172191
.unwrap_or_default();
173192
for (name, entry) in servers {
174-
merged.insert(name.clone(), entry.clone());
193+
merged.insert(name.clone(), gemini_mcp_entry(entry));
175194
}
176195
settings.insert("mcpServers".to_string(), serde_json::Value::Object(merged));
177196
Some(serde_json::Value::Object(settings).to_string())
@@ -468,6 +487,31 @@ mod mcp_settings_tests {
468487
},
469488
"linear": { "type": "http", "url": "https://mcp.linear.app/mcp" }
470489
});
490+
let request = request_with_mcp_servers(Some(servers));
491+
let settings = gemini_settings_with_mcp_servers(&request, None).expect("settings");
492+
assert_eq!(
493+
serde_json::from_str::<serde_json::Value>(&settings).expect("json"),
494+
json!({ "mcpServers": {
495+
"docs": {
496+
"command": "npx",
497+
"args": ["-y", "docs-mcp"],
498+
"env": { "TOKEN": "t" }
499+
},
500+
"linear": { "type": "http", "httpUrl": "https://mcp.linear.app/mcp" }
501+
}}),
502+
"http entries must carry httpUrl (gemini reads url as SSE)",
503+
);
504+
}
505+
506+
#[test]
507+
fn sse_entries_keep_url_untranslated() {
508+
let servers = json!({
509+
"linear": {
510+
"type": "sse",
511+
"url": "https://mcp.linear.app/sse",
512+
"headers": { "Authorization": "Bearer x" }
513+
}
514+
});
471515
let request = request_with_mcp_servers(Some(servers.clone()));
472516
let settings = gemini_settings_with_mcp_servers(&request, None).expect("settings");
473517
assert_eq!(

0 commit comments

Comments
 (0)