fix: auto-approve MCP server for Cursor CLI (#1028)
* fix: auto-approve MCP server for Cursor CLI Cursor CLI requires interactive approval for enabling MCP server per directory. There is no direct cli flag to auto-approve in non-interactive mode. * reslove symlinks
This commit is contained in:
@@ -46,3 +46,4 @@ agent-client-protocol = "0.4"
|
||||
codex-protocol = { git = "https://github.com/openai/codex.git", package = "codex-protocol", rev = "488ec061bf4d36916b8f477c700ea4fde4162a7a" }
|
||||
codex-app-server-protocol = { git = "https://github.com/openai/codex.git", package = "codex-app-server-protocol", rev = "488ec061bf4d36916b8f477c700ea4fde4162a7a" }
|
||||
codex-mcp-types = { git = "https://github.com/openai/codex.git", package = "mcp-types", rev = "488ec061bf4d36916b8f477c700ea4fde4162a7a" }
|
||||
sha2 = "0.10"
|
||||
|
||||
@@ -29,6 +29,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
mod mcp;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
|
||||
pub struct Cursor {
|
||||
#[serde(default)]
|
||||
@@ -63,6 +65,8 @@ impl Cursor {
|
||||
#[async_trait]
|
||||
impl StandardCodingAgentExecutor for Cursor {
|
||||
async fn spawn(&self, current_dir: &Path, prompt: &str) -> Result<SpawnedChild, ExecutorError> {
|
||||
mcp::ensure_mcp_server_trust(self, current_dir).await;
|
||||
|
||||
let (shell_cmd, shell_arg) = get_shell_command();
|
||||
let agent_cmd = self.build_command_builder().build_initial();
|
||||
|
||||
@@ -94,6 +98,8 @@ impl StandardCodingAgentExecutor for Cursor {
|
||||
prompt: &str,
|
||||
session_id: &str,
|
||||
) -> Result<SpawnedChild, ExecutorError> {
|
||||
mcp::ensure_mcp_server_trust(self, current_dir).await;
|
||||
|
||||
let (shell_cmd, shell_arg) = get_shell_command();
|
||||
let agent_cmd = self
|
||||
.build_command_builder()
|
||||
|
||||
172
crates/executors/src/executors/cursor/mcp.rs
Normal file
172
crates/executors/src/executors/cursor/mcp.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use std::{collections::HashSet, env, io::ErrorKind, path::Path};
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::fs;
|
||||
use tracing::warn;
|
||||
|
||||
use super::Cursor;
|
||||
use crate::executors::{CodingAgent, ExecutorError, StandardCodingAgentExecutor};
|
||||
|
||||
pub async fn ensure_mcp_server_trust(cursor: &Cursor, current_dir: &Path) {
|
||||
if let Err(err) = ensure_mcp_server_trust_impl(cursor, current_dir).await {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"Cursor MCP approval bootstrap failed. MCP servers might be unavailable."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_mcp_server_trust_impl(
|
||||
cursor: &Cursor,
|
||||
current_dir: &Path,
|
||||
) -> Result<(), ExecutorError> {
|
||||
let current_dir =
|
||||
std::fs::canonicalize(current_dir).unwrap_or_else(|_| current_dir.to_path_buf());
|
||||
|
||||
let Some(config_path) = cursor.default_mcp_config_path() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(home_dir) = dirs::home_dir() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let absolute_path = if current_dir.is_absolute() {
|
||||
current_dir.to_path_buf()
|
||||
} else {
|
||||
match env::current_dir() {
|
||||
Ok(cwd) => cwd.join(current_dir),
|
||||
Err(_) => current_dir.to_path_buf(),
|
||||
}
|
||||
};
|
||||
|
||||
let worktree_path_str = absolute_path.to_string_lossy().to_string();
|
||||
if worktree_path_str.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(project_slug) = cursor_project_slug(&absolute_path) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let config_value: serde_json::Value = match fs::read_to_string(&config_path).await {
|
||||
Ok(content) => match serde_json::from_str(&content) {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
error = ?err,
|
||||
path = %config_path.display(),
|
||||
"Failed to parse Cursor MCP config; falling back to defaults for auto-approval bootstrap"
|
||||
);
|
||||
default_cursor_mcp_servers(cursor)
|
||||
}
|
||||
},
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => default_cursor_mcp_servers(cursor),
|
||||
Err(err) => return Err(ExecutorError::Io(err)),
|
||||
};
|
||||
|
||||
let Some(servers) = config_value
|
||||
.get("mcpServers")
|
||||
.and_then(|value| value.as_object())
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let approvals_path = home_dir
|
||||
.join(".cursor")
|
||||
.join("projects")
|
||||
.join(&project_slug)
|
||||
.join("mcp-approvals.json");
|
||||
|
||||
let mut existing: Vec<String> = match fs::read_to_string(&approvals_path).await {
|
||||
Ok(content) => match serde_json::from_str(&content) {
|
||||
Ok(list) => list,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
error = ?err,
|
||||
path = %approvals_path.display(),
|
||||
"Failed to parse existing Cursor MCP approvals; resetting file"
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => Vec::new(),
|
||||
Err(err) => return Err(ExecutorError::Io(err)),
|
||||
};
|
||||
|
||||
let mut approvals_set: HashSet<String> = existing.iter().cloned().collect();
|
||||
let mut newly_added = Vec::new();
|
||||
|
||||
for (server_name, definition) in servers {
|
||||
if server_name == "meta" || !definition.is_object() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(approval_id) =
|
||||
compute_cursor_approval_id(server_name, definition, &worktree_path_str)
|
||||
&& approvals_set.insert(approval_id.clone())
|
||||
{
|
||||
newly_added.push(approval_id);
|
||||
}
|
||||
}
|
||||
|
||||
if newly_added.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
existing.extend(newly_added);
|
||||
|
||||
if let Some(parent) = approvals_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.await
|
||||
.map_err(ExecutorError::Io)?;
|
||||
}
|
||||
|
||||
let serialized = serde_json::to_string_pretty(&existing)?;
|
||||
fs::write(&approvals_path, serialized)
|
||||
.await
|
||||
.map_err(ExecutorError::Io)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cursor_project_slug(path: &Path) -> Option<String> {
|
||||
let raw = path.to_string_lossy();
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let slug = regex::Regex::new(r"[^A-Za-z0-9]+")
|
||||
.unwrap()
|
||||
.replace_all(&raw, "-")
|
||||
.trim_matches('-')
|
||||
.to_string();
|
||||
|
||||
if slug.is_empty() { None } else { Some(slug) }
|
||||
}
|
||||
|
||||
fn compute_cursor_approval_id(
|
||||
server_name: &str,
|
||||
definition: &serde_json::Value,
|
||||
worktree_path: &str,
|
||||
) -> Option<String> {
|
||||
let payload = serde_json::json!({
|
||||
"path": worktree_path,
|
||||
"server": definition,
|
||||
});
|
||||
|
||||
let serialized = serde_json::to_string(&payload).ok()?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(serialized.as_bytes());
|
||||
let digest = hasher.finalize();
|
||||
let hex = digest
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<String>();
|
||||
Some(format!("{server_name}-{}", &hex[..16]))
|
||||
}
|
||||
|
||||
fn default_cursor_mcp_servers(cursor: &Cursor) -> serde_json::Value {
|
||||
let mcpc = CodingAgent::Cursor(cursor.clone()).get_mcp_config();
|
||||
serde_json::json!({ "mcpServers": mcpc.preconfigured })
|
||||
}
|
||||
Reference in New Issue
Block a user