use async_trait::async_trait;
use everruns_core::ToolHints;
use everruns_core::tools::{Tool, ToolExecutionResult};
use everruns_core::traits::ToolContext;
use serde_json::{Value, json};
use tracing::{debug, error};
use crate::client::{CursorClient, LaunchAgentRequest};
use crate::{CURSOR_API_BASE_ENV, CURSOR_API_KEY_SECRET, CURSOR_CONNECTION_PROVIDER};
const MAX_PROMPT_CHARS: usize = 20_000;
const MAX_AGENT_ID_CHARS: usize = 128;
const MAX_REF_CHARS: usize = 255;
const MAX_BRANCH_CHARS: usize = 255;
const MAX_MODEL_CHARS: usize = 128;
async fn get_api_key(context: &ToolContext) -> Result<String, ToolExecutionResult> {
if let Some(resolver) = context.connection_resolver.as_ref() {
match resolver
.get_connection_token(context.session_id, CURSOR_CONNECTION_PROVIDER)
.await
{
Ok(Some(token)) if !token.trim().is_empty() => return Ok(token),
Ok(_) => {}
Err(e) => debug!("Cursor connection resolver failed: {e}"),
}
}
if let Some(storage) = context.storage_store.as_ref() {
match storage
.get_secret(context.session_id, CURSOR_API_KEY_SECRET)
.await
{
Ok(Some(key)) if !key.trim().is_empty() => return Ok(key),
Ok(_) => {}
Err(e) => {
error!("Failed to read {CURSOR_API_KEY_SECRET} session secret: {e}");
return Err(ToolExecutionResult::internal_error_msg(
"Failed to read Cursor API key",
));
}
}
}
Err(ToolExecutionResult::connection_required(
CURSOR_CONNECTION_PROVIDER,
))
}
fn cursor_client(api_key: String) -> CursorClient {
match std::env::var(CURSOR_API_BASE_ENV) {
Ok(api_base) if !api_base.trim().is_empty() => {
CursorClient::with_base_url(api_key, api_base)
}
_ => CursorClient::new(api_key),
}
}
fn required_str<'a>(arguments: &'a Value, name: &str) -> Result<&'a str, ToolExecutionResult> {
arguments
.get(name)
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|v| !v.is_empty())
.ok_or_else(|| {
ToolExecutionResult::tool_error(format!("Missing required parameter: {name}"))
})
}
fn optional_str<'a>(
arguments: &'a Value,
name: &str,
max_chars: usize,
) -> Result<Option<&'a str>, ToolExecutionResult> {
match arguments.get(name) {
Some(Value::String(value)) => {
let trimmed = value.trim();
if trimmed.is_empty() {
Ok(None)
} else if trimmed.chars().count() > max_chars {
Err(ToolExecutionResult::tool_error(format!(
"Invalid '{name}': maximum length is {max_chars} characters"
)))
} else {
Ok(Some(trimmed))
}
}
Some(Value::Null) | None => Ok(None),
Some(_) => Err(ToolExecutionResult::tool_error(format!(
"Invalid '{name}': must be a string"
))),
}
}
fn required_limited_str<'a>(
arguments: &'a Value,
name: &str,
max_chars: usize,
) -> Result<&'a str, ToolExecutionResult> {
let value = required_str(arguments, name)?;
if value.chars().count() > max_chars {
return Err(ToolExecutionResult::tool_error(format!(
"Invalid '{name}': maximum length is {max_chars} characters"
)));
}
Ok(value)
}
fn optional_bool(arguments: &Value, name: &str) -> Result<Option<bool>, ToolExecutionResult> {
match arguments.get(name) {
Some(Value::Bool(value)) => Ok(Some(*value)),
Some(Value::Null) | None => Ok(None),
Some(_) => Err(ToolExecutionResult::tool_error(format!(
"Invalid '{name}': must be a boolean"
))),
}
}
fn optional_limit(arguments: &Value) -> Result<Option<u32>, ToolExecutionResult> {
match arguments.get("limit") {
Some(Value::Number(n)) => {
let Some(value) = n.as_u64() else {
return Err(ToolExecutionResult::tool_error(
"Invalid 'limit': must be an integer between 1 and 100",
));
};
if !(1..=100).contains(&value) {
return Err(ToolExecutionResult::tool_error(
"Invalid 'limit': must be between 1 and 100",
));
}
Ok(Some(value as u32))
}
Some(Value::Null) | None => Ok(None),
Some(_) => Err(ToolExecutionResult::tool_error(
"Invalid 'limit': must be an integer between 1 and 100",
)),
}
}
fn context_required_error(name: &str) -> ToolExecutionResult {
ToolExecutionResult::tool_error(format!(
"{name} requires context. This tool must be executed with session context."
))
}
pub struct CursorLaunchAgentTool;
#[async_trait]
impl Tool for CursorLaunchAgentTool {
fn name(&self) -> &str {
"cursor_launch_agent"
}
fn display_name(&self) -> Option<&str> {
Some("Launch Cursor Agent")
}
fn description(&self) -> &str {
"Start a Cursor Cloud Agent to work asynchronously on a GitHub repository."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"prompt": { "type": "string", "description": "Precise task instructions for the Cursor agent.", "minLength": 1, "maxLength": MAX_PROMPT_CHARS },
"repository": { "type": "string", "description": "GitHub repository URL, e.g. https://github.com/org/repo.", "minLength": 1 },
"ref": { "type": "string", "description": "Base branch, tag, or ref. Default is Cursor's repository default branch." },
"model": { "type": "string", "description": "Optional Cursor model id. Omit for Cursor Auto." },
"auto_create_pr": { "type": "boolean", "description": "Whether Cursor should create a pull request when the agent finishes. Default false." },
"branch_name": { "type": "string", "description": "Optional custom branch name for Cursor to create." },
"name": { "type": "string", "description": "Optional short human-readable task name used only for UI narration." }
},
"required": ["prompt", "repository"],
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_open_world(true)
.with_requires_secrets(true)
.with_long_running(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
context_required_error("cursor_launch_agent")
}
fn requires_context(&self) -> bool {
true
}
async fn execute_with_context(
&self,
arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let prompt = match required_limited_str(&arguments, "prompt", MAX_PROMPT_CHARS) {
Ok(v) => v,
Err(e) => return e,
};
let repository = match required_str(&arguments, "repository") {
Ok(v) => v,
Err(e) => return e,
};
let ref_ = match optional_str(&arguments, "ref", MAX_REF_CHARS) {
Ok(v) => v.map(str::to_string),
Err(e) => return e,
};
let model = match optional_str(&arguments, "model", MAX_MODEL_CHARS) {
Ok(v) => v.map(str::to_string),
Err(e) => return e,
};
let branch_name = match optional_str(&arguments, "branch_name", MAX_BRANCH_CHARS) {
Ok(v) => v.map(str::to_string),
Err(e) => return e,
};
let auto_create_pr = match optional_bool(&arguments, "auto_create_pr") {
Ok(v) => v,
Err(e) => return e,
};
let api_key = match get_api_key(context).await {
Ok(v) => v,
Err(e) => return e,
};
match cursor_client(api_key)
.launch_agent(LaunchAgentRequest {
prompt: prompt.to_string(),
repository: repository.to_string(),
ref_,
model,
auto_create_pr,
branch_name,
})
.await
{
Ok(agent) => ToolExecutionResult::success(json!(agent)),
Err(e) => ToolExecutionResult::tool_error(e),
}
}
}
pub struct CursorGetAgentTool;
#[async_trait]
impl Tool for CursorGetAgentTool {
fn name(&self) -> &str {
"cursor_get_agent"
}
fn display_name(&self) -> Option<&str> {
Some("Get Cursor Agent")
}
fn description(&self) -> &str {
"Get status and result metadata for a Cursor Cloud Agent."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Cursor agent id, e.g. bc_abc123.", "minLength": 1, "maxLength": MAX_AGENT_ID_CHARS }
},
"required": ["agent_id"],
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_readonly(true)
.with_idempotent(true)
.with_open_world(true)
.with_requires_secrets(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
context_required_error("cursor_get_agent")
}
fn requires_context(&self) -> bool {
true
}
async fn execute_with_context(
&self,
arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let agent_id = match required_limited_str(&arguments, "agent_id", MAX_AGENT_ID_CHARS) {
Ok(v) => v,
Err(e) => return e,
};
let api_key = match get_api_key(context).await {
Ok(v) => v,
Err(e) => return e,
};
match cursor_client(api_key).get_agent(agent_id).await {
Ok(agent) => ToolExecutionResult::success(json!(agent)),
Err(e) => ToolExecutionResult::tool_error(e),
}
}
}
pub struct CursorListAgentsTool;
#[async_trait]
impl Tool for CursorListAgentsTool {
fn name(&self) -> &str {
"cursor_list_agents"
}
fn display_name(&self) -> Option<&str> {
Some("List Cursor Agents")
}
fn description(&self) -> &str {
"List Cursor Cloud Agents for the authenticated Cursor account."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"limit": { "type": "integer", "description": "Number of agents to return, 1-100. Default 20.", "minimum": 1, "maximum": 100 },
"cursor": { "type": "string", "description": "Pagination cursor from a previous response." }
},
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_readonly(true)
.with_idempotent(true)
.with_open_world(true)
.with_requires_secrets(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
context_required_error("cursor_list_agents")
}
fn requires_context(&self) -> bool {
true
}
async fn execute_with_context(
&self,
arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let limit = match optional_limit(&arguments) {
Ok(v) => v,
Err(e) => return e,
};
let cursor = match optional_str(&arguments, "cursor", MAX_AGENT_ID_CHARS) {
Ok(v) => v,
Err(e) => return e,
};
let api_key = match get_api_key(context).await {
Ok(v) => v,
Err(e) => return e,
};
match cursor_client(api_key).list_agents(limit, cursor).await {
Ok(response) => ToolExecutionResult::success(json!(response)),
Err(e) => ToolExecutionResult::tool_error(e),
}
}
}
pub struct CursorAddFollowupTool;
#[async_trait]
impl Tool for CursorAddFollowupTool {
fn name(&self) -> &str {
"cursor_add_followup"
}
fn display_name(&self) -> Option<&str> {
Some("Add Cursor Follow-up")
}
fn description(&self) -> &str {
"Send additional instructions to a running Cursor Cloud Agent."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Cursor agent id.", "minLength": 1, "maxLength": MAX_AGENT_ID_CHARS },
"prompt": { "type": "string", "description": "Follow-up instruction.", "minLength": 1, "maxLength": MAX_PROMPT_CHARS }
},
"required": ["agent_id", "prompt"],
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_open_world(true)
.with_requires_secrets(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
context_required_error("cursor_add_followup")
}
fn requires_context(&self) -> bool {
true
}
async fn execute_with_context(
&self,
arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let agent_id = match required_limited_str(&arguments, "agent_id", MAX_AGENT_ID_CHARS) {
Ok(v) => v,
Err(e) => return e,
};
let prompt = match required_limited_str(&arguments, "prompt", MAX_PROMPT_CHARS) {
Ok(v) => v,
Err(e) => return e,
};
let api_key = match get_api_key(context).await {
Ok(v) => v,
Err(e) => return e,
};
match cursor_client(api_key).add_followup(agent_id, prompt).await {
Ok(response) => ToolExecutionResult::success(response),
Err(e) => ToolExecutionResult::tool_error(e),
}
}
}
pub struct CursorGetConversationTool;
#[async_trait]
impl Tool for CursorGetConversationTool {
fn name(&self) -> &str {
"cursor_get_conversation"
}
fn display_name(&self) -> Option<&str> {
Some("Get Cursor Conversation")
}
fn description(&self) -> &str {
"Retrieve the conversation transcript for a Cursor Cloud Agent."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Cursor agent id.", "minLength": 1, "maxLength": MAX_AGENT_ID_CHARS }
},
"required": ["agent_id"],
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_readonly(true)
.with_idempotent(true)
.with_open_world(true)
.with_requires_secrets(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
context_required_error("cursor_get_conversation")
}
fn requires_context(&self) -> bool {
true
}
async fn execute_with_context(
&self,
arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let agent_id = match required_limited_str(&arguments, "agent_id", MAX_AGENT_ID_CHARS) {
Ok(v) => v,
Err(e) => return e,
};
let api_key = match get_api_key(context).await {
Ok(v) => v,
Err(e) => return e,
};
match cursor_client(api_key).get_conversation(agent_id).await {
Ok(response) => ToolExecutionResult::success(json!(response)),
Err(e) => ToolExecutionResult::tool_error(e),
}
}
}
pub struct CursorDeleteAgentTool;
#[async_trait]
impl Tool for CursorDeleteAgentTool {
fn name(&self) -> &str {
"cursor_delete_agent"
}
fn display_name(&self) -> Option<&str> {
Some("Delete Cursor Agent")
}
fn description(&self) -> &str {
"Permanently delete a Cursor Cloud Agent record and associated resources."
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Cursor agent id.", "minLength": 1, "maxLength": MAX_AGENT_ID_CHARS }
},
"required": ["agent_id"],
"additionalProperties": false
})
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_destructive(true)
.with_open_world(true)
.with_requires_secrets(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
context_required_error("cursor_delete_agent")
}
fn requires_context(&self) -> bool {
true
}
async fn execute_with_context(
&self,
arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let agent_id = match required_limited_str(&arguments, "agent_id", MAX_AGENT_ID_CHARS) {
Ok(v) => v,
Err(e) => return e,
};
let api_key = match get_api_key(context).await {
Ok(v) => v,
Err(e) => return e,
};
match cursor_client(api_key).delete_agent(agent_id).await {
Ok(response) => ToolExecutionResult::success(response),
Err(e) => ToolExecutionResult::tool_error(e),
}
}
}
pub struct CursorListModelsTool;
#[async_trait]
impl Tool for CursorListModelsTool {
fn name(&self) -> &str {
"cursor_list_models"
}
fn display_name(&self) -> Option<&str> {
Some("List Cursor Models")
}
fn description(&self) -> &str {
"List model ids recommended by Cursor for Cloud Agents."
}
fn parameters_schema(&self) -> Value {
json!({ "type": "object", "properties": {}, "additionalProperties": false })
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_readonly(true)
.with_idempotent(true)
.with_open_world(true)
.with_requires_secrets(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
context_required_error("cursor_list_models")
}
fn requires_context(&self) -> bool {
true
}
async fn execute_with_context(
&self,
_arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let api_key = match get_api_key(context).await {
Ok(v) => v,
Err(e) => return e,
};
match cursor_client(api_key).list_models().await {
Ok(response) => ToolExecutionResult::success(json!(response)),
Err(e) => ToolExecutionResult::tool_error(e),
}
}
}
pub struct CursorListRepositoriesTool;
#[async_trait]
impl Tool for CursorListRepositoriesTool {
fn name(&self) -> &str {
"cursor_list_repositories"
}
fn display_name(&self) -> Option<&str> {
Some("List Cursor Repositories")
}
fn description(&self) -> &str {
"List GitHub repositories accessible to Cursor. This Cursor endpoint is heavily rate-limited; use sparingly."
}
fn parameters_schema(&self) -> Value {
json!({ "type": "object", "properties": {}, "additionalProperties": false })
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_readonly(true)
.with_open_world(true)
.with_requires_secrets(true)
.with_long_running(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
context_required_error("cursor_list_repositories")
}
fn requires_context(&self) -> bool {
true
}
async fn execute_with_context(
&self,
_arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let api_key = match get_api_key(context).await {
Ok(v) => v,
Err(e) => return e,
};
match cursor_client(api_key).list_repositories().await {
Ok(response) => ToolExecutionResult::success(json!(response)),
Err(e) => ToolExecutionResult::tool_error(e),
}
}
}
pub struct CursorKeyInfoTool;
#[async_trait]
impl Tool for CursorKeyInfoTool {
fn name(&self) -> &str {
"cursor_key_info"
}
fn display_name(&self) -> Option<&str> {
Some("Check Cursor Connection")
}
fn description(&self) -> &str {
"Check Cursor API key metadata for the active connection."
}
fn parameters_schema(&self) -> Value {
json!({ "type": "object", "properties": {}, "additionalProperties": false })
}
fn hints(&self) -> ToolHints {
ToolHints::default()
.with_readonly(true)
.with_idempotent(true)
.with_open_world(true)
.with_requires_secrets(true)
}
async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
context_required_error("cursor_key_info")
}
fn requires_context(&self) -> bool {
true
}
async fn execute_with_context(
&self,
_arguments: Value,
context: &ToolContext,
) -> ToolExecutionResult {
let api_key = match get_api_key(context).await {
Ok(v) => v,
Err(e) => return e,
};
match cursor_client(api_key).api_key_info().await {
Ok(response) => ToolExecutionResult::success(json!(response)),
Err(e) => ToolExecutionResult::tool_error(e),
}
}
}
#[cfg(test)]
mod auth_tests {
use super::*;
use everruns_core::SessionId;
#[tokio::test]
async fn get_api_key_does_not_fall_back_to_global_env_var() {
unsafe { std::env::set_var(CURSOR_API_KEY_SECRET, "should-not-be-used") };
let ctx = ToolContext::new(SessionId::new());
let err = get_api_key(&ctx).await.unwrap_err();
unsafe { std::env::remove_var(CURSOR_API_KEY_SECRET) };
match err {
ToolExecutionResult::ConnectionRequired { provider } => {
assert_eq!(provider, CURSOR_CONNECTION_PROVIDER);
}
other => panic!("expected ConnectionRequired, got {other:?}"),
}
}
}