use std::path::{Path, PathBuf};
use serde_json::{Map, Value};
fn build_mcp_endpoint(
workspace_id: &str,
session_id: &str,
tool_mode: Option<&str>,
mcp_profile: Option<&str>,
) -> String {
let base_url =
std::env::var("ROUTA_SERVER_URL").unwrap_or_else(|_| "http://127.0.0.1:3210".to_string());
let mut params = vec![
format!("wsId={}", workspace_id),
format!("sid={}", session_id),
];
if let Some(mode) = tool_mode.filter(|value| *value == "essential" || *value == "full") {
params.push(format!("toolMode={}", mode));
}
if let Some(profile) =
mcp_profile.filter(|value| *value == "kanban-planning" || *value == "team-coordination")
{
params.push(format!("mcpProfile={}", profile));
}
format!("{}/api/mcp?{}", base_url, params.join("&"))
}
pub fn build_claude_mcp_config(
workspace_id: &str,
session_id: &str,
tool_mode: Option<&str>,
mcp_profile: Option<&str>,
) -> String {
serde_json::json!({
"mcpServers": {
"routa-coordination": {
"url": build_mcp_endpoint(workspace_id, session_id, tool_mode, mcp_profile),
"type": "http",
"env": {
"ROUTA_WORKSPACE_ID": workspace_id,
},
}
}
})
.to_string()
}
pub fn build_acp_http_mcp_servers(
workspace_id: &str,
session_id: &str,
tool_mode: Option<&str>,
mcp_profile: Option<&str>,
) -> Vec<serde_json::Value> {
vec![serde_json::json!({
"type": "http",
"name": "routa-coordination",
"url": build_mcp_endpoint(workspace_id, session_id, tool_mode, mcp_profile),
"headers": []
})]
}
async fn ensure_mcp_for_opencode(
workspace_id: &str,
session_id: &str,
tool_mode: Option<&str>,
mcp_profile: Option<&str>,
) -> Result<String, String> {
let home_dir =
dirs::home_dir().ok_or_else(|| "Failed to resolve home directory".to_string())?;
let config_dir = home_dir.join(".config").join("opencode");
let config_file = config_dir.join("opencode.json");
let mut existing: Map<String, Value> = match tokio::fs::read_to_string(&config_file).await {
Ok(raw) => serde_json::from_str::<Value>(&raw)
.ok()
.and_then(|value| value.as_object().cloned())
.unwrap_or_default(),
Err(_) => Map::new(),
};
let mut mcp = existing
.remove("mcp")
.and_then(|value| value.as_object().cloned())
.unwrap_or_default();
mcp.insert(
"routa-coordination".to_string(),
serde_json::json!({
"type": "remote",
"url": build_mcp_endpoint(workspace_id, session_id, tool_mode, mcp_profile),
"enabled": true
}),
);
existing.insert("mcp".to_string(), Value::Object(mcp));
tokio::fs::create_dir_all(&config_dir)
.await
.map_err(|err| format!("mkdir {}: {}", config_dir.display(), err))?;
let encoded = serde_json::to_vec_pretty(&Value::Object(existing))
.map_err(|err| format!("encode OpenCode MCP config: {}", err))?;
tokio::fs::write(&config_file, encoded)
.await
.map_err(|err| format!("write {}: {}", config_file.display(), err))?;
Ok(format!(
"opencode: wrote MCP config to {}",
display_path(&config_file)
))
}
fn codex_config_path_for_home(home_dir: &Path) -> PathBuf {
home_dir.join(".routa").join("codex").join("config.toml")
}
fn codex_private_config_path() -> Result<PathBuf, String> {
let home_dir =
dirs::home_dir().ok_or_else(|| "Failed to resolve home directory".to_string())?;
Ok(codex_config_path_for_home(&home_dir))
}
fn build_codex_mcp_config_contents(
workspace_id: &str,
session_id: &str,
tool_mode: Option<&str>,
mcp_profile: Option<&str>,
) -> String {
let endpoint = build_mcp_endpoint(workspace_id, session_id, tool_mode, mcp_profile);
format!(
"[mcp_servers.routa-coordination]\nurl = \"{}\"\nenabled = true\n",
endpoint
)
}
fn upsert_codex_mcp_section(existing: &str, rendered_section: &str) -> String {
let section_header = "[mcp_servers.routa-coordination]";
if let Some(start) = existing.find(section_header) {
let after_header = &existing[start + section_header.len()..];
let next_section_offset = after_header
.find("\n[")
.map(|offset| start + section_header.len() + offset + 1);
let end = next_section_offset.unwrap_or(existing.len());
let mut updated = String::with_capacity(existing.len() + rendered_section.len());
updated.push_str(existing[..start].trim_end());
if !updated.is_empty() {
updated.push_str("\n\n");
}
updated.push_str(rendered_section.trim_end());
if end < existing.len() {
updated.push_str("\n\n");
updated.push_str(existing[end..].trim_start());
} else {
updated.push('\n');
}
return updated;
}
let trimmed = existing.trim_end();
if trimmed.is_empty() {
format!("{}\n", rendered_section.trim_end())
} else {
format!("{}\n\n{}\n", trimmed, rendered_section.trim_end())
}
}
async fn ensure_mcp_for_codex_at(
config_file: &Path,
workspace_id: &str,
session_id: &str,
tool_mode: Option<&str>,
mcp_profile: Option<&str>,
) -> Result<String, String> {
let config_dir = config_file
.parent()
.ok_or_else(|| format!("Invalid Codex config path: {}", config_file.display()))?;
let existing = tokio::fs::read_to_string(&config_file)
.await
.unwrap_or_default();
let rendered_section =
build_codex_mcp_config_contents(workspace_id, session_id, tool_mode, mcp_profile);
let updated = upsert_codex_mcp_section(&existing, &rendered_section);
tokio::fs::create_dir_all(config_dir)
.await
.map_err(|err| format!("mkdir {}: {}", config_dir.display(), err))?;
tokio::fs::write(&config_file, updated)
.await
.map_err(|err| format!("write {}: {}", config_file.display(), err))?;
Ok(format!(
"codex-acp: wrote private MCP config to {}",
display_path(config_file)
))
}
async fn ensure_mcp_for_codex(
workspace_id: &str,
session_id: &str,
tool_mode: Option<&str>,
mcp_profile: Option<&str>,
) -> Result<String, String> {
let config_file = codex_private_config_path()?;
ensure_mcp_for_codex_at(&config_file, workspace_id, session_id, tool_mode, mcp_profile).await
}
pub fn codex_project_trust_override(cwd: &str) -> String {
let escaped = cwd.replace('\\', "\\\\").replace('"', "\\\"");
format!("projects.\"{}\".trust_level=\"trusted\"", escaped)
}
fn codex_extract_routa_section_value(contents: &str, key: &str) -> Option<String> {
let section_header = "[mcp_servers.routa-coordination]";
let start = contents.find(section_header)?;
let after_header = &contents[start + section_header.len()..];
let next_section_offset = after_header
.find("\n[")
.map(|offset| start + section_header.len() + offset + 1);
let end = next_section_offset.unwrap_or(contents.len());
let section = &contents[start + section_header.len()..end];
section.lines().find_map(|line| {
let trimmed = line.trim();
let expected_prefix = format!("{key} = ");
let raw_value = trimmed.strip_prefix(&expected_prefix)?.trim();
Some(raw_value.trim_matches('"').to_string())
})
}
fn codex_cli_overrides_from_config(config_file: &Path, cwd: &str) -> Result<Vec<String>, String> {
let contents = std::fs::read_to_string(config_file)
.map_err(|err| format!("read {}: {}", config_file.display(), err))?;
let endpoint = codex_extract_routa_section_value(&contents, "url").ok_or_else(|| {
format!(
"Missing mcp_servers.routa-coordination.url in {}",
config_file.display()
)
})?;
let enabled = codex_extract_routa_section_value(&contents, "enabled")
.map(|value| value == "true")
.unwrap_or(true);
let escaped_endpoint = endpoint.replace('\\', "\\\\").replace('"', "\\\"");
Ok(vec![
codex_project_trust_override(cwd),
format!(
"mcp_servers.routa-coordination.url=\"{}\"",
escaped_endpoint
),
format!("mcp_servers.routa-coordination.enabled={enabled}"),
])
}
pub fn codex_cli_overrides(cwd: &str) -> Result<Vec<String>, String> {
let config_file = codex_private_config_path()?;
codex_cli_overrides_from_config(&config_file, cwd)
}
fn display_path(path: &Path) -> String {
path.to_string_lossy().to_string()
}
pub async fn ensure_mcp_for_provider(
provider_id: &str,
cwd: &str,
workspace_id: &str,
session_id: &str,
tool_mode: Option<&str>,
mcp_profile: Option<&str>,
) -> Result<Option<String>, String> {
let base_id = provider_id.strip_suffix("-registry").unwrap_or(provider_id);
match base_id {
"opencode" => ensure_mcp_for_opencode(workspace_id, session_id, tool_mode, mcp_profile)
.await
.map(Some),
"codex" | "codex-acp" => {
let _ = cwd;
ensure_mcp_for_codex(workspace_id, session_id, tool_mode, mcp_profile)
.await
.map(Some)
}
_ => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::{
build_acp_http_mcp_servers, build_claude_mcp_config, build_codex_mcp_config_contents,
build_mcp_endpoint, codex_cli_overrides_from_config, codex_config_path_for_home,
codex_project_trust_override, ensure_mcp_for_codex_at, upsert_codex_mcp_section,
};
#[test]
fn team_coordination_profile_is_forwarded_in_mcp_endpoint() {
let endpoint = build_mcp_endpoint(
"default",
"session-123",
Some("essential"),
Some("team-coordination"),
);
assert!(endpoint.contains("wsId=default"));
assert!(endpoint.contains("sid=session-123"));
assert!(endpoint.contains("toolMode=essential"));
assert!(endpoint.contains("mcpProfile=team-coordination"));
}
#[test]
fn claude_inline_config_uses_routa_coordination_server() {
let config = build_claude_mcp_config(
"default",
"session-123",
Some("essential"),
Some("team-coordination"),
);
assert!(config.contains("\"routa-coordination\""));
assert!(config.contains("\"type\":\"http\""));
assert!(config.contains("mcpProfile=team-coordination"));
}
#[test]
fn acp_http_mcp_servers_use_streamable_http_shape() {
let servers = build_acp_http_mcp_servers(
"default",
"session-123",
Some("full"),
Some("kanban-planning"),
);
assert_eq!(servers.len(), 1);
assert_eq!(servers[0]["type"], "http");
assert_eq!(servers[0]["name"], "routa-coordination");
assert_eq!(servers[0]["headers"], serde_json::json!([]));
assert!(servers[0]["url"]
.as_str()
.is_some_and(|url| url.contains("mcpProfile=kanban-planning")));
}
#[test]
fn codex_trust_override_marks_worktree_as_trusted() {
let override_arg = codex_project_trust_override("/tmp/example/project");
assert_eq!(
override_arg,
"projects.\"/tmp/example/project\".trust_level=\"trusted\""
);
}
#[tokio::test]
async fn codex_cli_overrides_include_trust_and_mcp_server() {
let tempdir = tempfile::tempdir().expect("tempdir");
let config_path = codex_config_path_for_home(tempdir.path());
ensure_mcp_for_codex_at(
&config_path,
"default",
"session-123",
Some("full"),
Some("kanban-planning"),
)
.await
.expect("ensure codex mcp")
;
let overrides = codex_cli_overrides_from_config(&config_path, "/tmp/example/project")
.expect("cli overrides");
assert_eq!(overrides.len(), 3);
assert_eq!(
overrides[0],
"projects.\"/tmp/example/project\".trust_level=\"trusted\""
);
assert!(overrides[1]
.contains("mcp_servers.routa-coordination.url=\"http://127.0.0.1:3210/api/mcp?"));
assert!(overrides[1].contains("wsId=default"));
assert!(overrides[1].contains("sid=session-123"));
assert!(overrides[1].contains("toolMode=full"));
assert!(overrides[1].contains("mcpProfile=kanban-planning"));
assert_eq!(overrides[2], "mcp_servers.routa-coordination.enabled=true");
}
#[test]
fn codex_config_upsert_replaces_existing_routa_section() {
let existing = "[mcp_servers.routa-coordination]\nurl = \"http://old\"\nenabled = true\n\n[model_providers.test]\nname = \"test\"\n";
let replacement = build_codex_mcp_config_contents(
"default",
"session-123",
Some("full"),
Some("kanban-planning"),
);
let updated = upsert_codex_mcp_section(existing, &replacement);
assert!(updated.contains("sid=session-123"));
assert!(!updated.contains("http://old"));
assert!(updated.contains("[model_providers.test]"));
}
#[tokio::test]
async fn codex_provider_writes_private_overlay_config() {
let tempdir = tempfile::tempdir().expect("tempdir");
let config_path = codex_config_path_for_home(tempdir.path());
let summary = ensure_mcp_for_codex_at(
&config_path,
"default",
"session-123",
Some("full"),
Some("kanban-planning"),
)
.await
.expect("ensure codex mcp");
let written = std::fs::read_to_string(&config_path).expect("read codex config");
assert!(summary.contains(".routa/codex/config.toml"));
assert!(written.contains("[mcp_servers.routa-coordination]"));
assert!(written.contains("wsId=default"));
assert!(written.contains("sid=session-123"));
assert!(written.contains("mcpProfile=kanban-planning"));
}
}