use crate::protocol::BackendKind;
use crate::resource_context::{
build_http_session_payload, build_visible_tool_context, ResourceRequestContext,
};
use crate::tool_defs::{
default_budget_for_profile, preferred_bootstrap_tools, ToolPreset, ToolProfile, ToolSurface,
};
use crate::tool_runtime::{required_string, success_meta, ToolResult};
use crate::AppState;
use codelens_engine::memory::list_memory_names;
use codelens_engine::{compute_dominant_language, detect_frameworks};
use serde_json::json;
pub fn activate_project(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
let switched = if let Some(path) = arguments.get("project").and_then(|v| v.as_str()) {
match state.switch_project(path) {
Ok(name) => Some(name),
Err(e) => {
return Err(crate::error::CodeLensError::NotFound(format!(
"failed to switch project: {e}"
)));
}
}
} else {
None
};
let project = state.project();
let project_name = project
.as_path()
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let memories_dir = state.memories_dir();
let memory_count = list_memory_names(&memories_dir, None).len();
let watcher_running = state.watcher_running();
let frameworks = detect_frameworks(project.as_path());
let project_base_path = project.as_path().to_string_lossy().to_string();
auto_set_embed_hint_lang(project.as_path());
let session = crate::session_context::SessionRequestContext::from_json(arguments);
let client = session
.client_name
.as_deref()
.map(|name| crate::client_profile::ClientProfile::detect(Some(name)))
.unwrap_or_else(|| state.client_profile());
let file_count = state
.symbol_index()
.stats()
.map(|s| s.indexed_files)
.unwrap_or(0);
let (auto_surface, auto_budget, auto_label) =
if matches!(client, crate::client_profile::ClientProfile::Claude) {
(
ToolSurface::Preset(ToolPreset::Balanced),
client.default_budget(),
"balanced",
)
} else if file_count < 50 {
(
ToolSurface::Profile(ToolProfile::BuilderMinimal),
default_budget_for_profile(ToolProfile::BuilderMinimal)
.max(client.default_budget()),
"builder-minimal",
)
} else if file_count > 500 {
(
ToolSurface::Profile(ToolProfile::ReviewerGraph),
default_budget_for_profile(ToolProfile::ReviewerGraph).max(client.default_budget()),
"reviewer-graph",
)
} else {
(
ToolSurface::Profile(ToolProfile::PlannerReadonly),
default_budget_for_profile(ToolProfile::PlannerReadonly)
.max(client.default_budget()),
"planner-readonly",
)
};
#[cfg(feature = "http")]
if !session.is_local() {
state.set_session_surface_and_budget(&session.session_id, auto_surface, auto_budget);
state.bind_project_to_session(&session.session_id, &project_base_path);
} else {
state.set_surface(auto_surface);
state.set_token_budget(auto_budget);
}
#[cfg(not(feature = "http"))]
{
state.set_surface(auto_surface);
state.set_token_budget(auto_budget);
}
#[cfg(feature = "semantic")]
let embedding_ready = state.embedding_ref().is_some();
#[cfg(not(feature = "semantic"))]
let embedding_ready = false;
Ok((
json!({
"activated": true,
"switched": switched.is_some(),
"project_name": project_name,
"project_base_path": project_base_path,
"backend_id": "rust-core",
"memory_count": memory_count,
"serena_memories_dir": memories_dir.to_string_lossy(),
"file_watcher": watcher_running,
"frameworks": frameworks,
"auto_surface": auto_label,
"auto_budget": auto_budget,
"indexed_files": file_count,
"embedding_ready": embedding_ready
}),
success_meta(BackendKind::Session, 1.0),
))
}
pub fn prepare_harness_session(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
if arguments.get("preset").and_then(|v| v.as_str()).is_some()
&& arguments.get("profile").and_then(|v| v.as_str()).is_some()
{
return Err(crate::error::CodeLensError::Validation(
"prepare_harness_session accepts either `preset` or `profile`, not both".to_owned(),
));
}
let (activate_payload, _) = activate_project(state, arguments)?;
if let Some(effort_str) = arguments.get("effort_level").and_then(|v| v.as_str()) {
let level = match effort_str {
"low" => crate::client_profile::EffortLevel::Low,
"medium" => crate::client_profile::EffortLevel::Medium,
_ => crate::client_profile::EffortLevel::High,
};
state.set_effort_level(level);
}
if arguments.get("profile").and_then(|v| v.as_str()).is_some() {
crate::tools::session::set_profile(state, arguments)?;
} else if arguments.get("preset").and_then(|v| v.as_str()).is_some() {
crate::tools::session::set_preset(state, arguments)?;
} else if let Some(budget) = arguments
.get("token_budget")
.and_then(|v| v.as_u64())
.map(|v| v as usize)
{
#[cfg(feature = "http")]
{
let session = crate::session_context::SessionRequestContext::from_json(arguments);
if !session.is_local() {
state.set_session_surface_and_budget(
&session.session_id,
state.execution_surface(&session),
budget,
);
} else {
state.set_token_budget(budget);
}
}
#[cfg(not(feature = "http"))]
{
state.set_token_budget(budget);
}
}
let detail = arguments
.get("detail")
.and_then(|v| v.as_str())
.unwrap_or("compact");
let request = ResourceRequestContext::from_request("codelens://tools/list", Some(arguments));
let session = request.session.clone();
let active_surface = state.execution_surface(&session);
let token_budget = state.execution_token_budget(&session);
let config_payload = if detail == "full" {
let (payload, _) = crate::tools::filesystem::get_current_config(state, arguments)?;
payload
} else {
json!({
"runtime": "rust-core",
"project_root": state.project().as_path().display().to_string(),
"surface": active_surface.as_label(),
"token_budget": token_budget,
"tool_count": crate::tool_defs::visible_tools(active_surface).len(),
"client_profile": request.client_profile.as_str(),
})
};
let capabilities_arguments = match arguments.get("file_path").and_then(|v| v.as_str()) {
Some(file_path) => json!({ "file_path": file_path }),
None => json!({}),
};
let (capabilities_payload, _) =
crate::tools::session::get_capabilities(state, &capabilities_arguments)?;
let warnings = capabilities_payload
.get("daemon_binary_drift")
.and_then(|value| value.get("stale_daemon"))
.and_then(|value| value.as_bool())
.filter(|stale| *stale)
.map(|_| {
vec![json!({
"code": "stale_daemon_binary",
"message": "running daemon is older than the executable on disk; restart the MCP server to pick up the latest build",
"restart_recommended": true,
})]
})
.unwrap_or_default();
let visible = build_visible_tool_context(state, &request);
let visible_tool_names = visible
.tools
.iter()
.map(|tool| tool.name.to_owned())
.collect::<Vec<_>>();
let requested_entrypoints = arguments
.get("preferred_entrypoints")
.and_then(|value| value.as_array())
.map(|items| {
items
.iter()
.filter_map(|item| item.as_str())
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
})
.unwrap_or_default();
let preferred_entrypoints_source = if requested_entrypoints.is_empty() {
"surface_default"
} else {
"provided"
};
let preferred_entrypoints = if requested_entrypoints.is_empty() {
preferred_bootstrap_tools(active_surface)
.unwrap_or(&[])
.iter()
.map(|tool| (*tool).to_owned())
.collect::<Vec<_>>()
} else {
requested_entrypoints
};
let preferred_entrypoints_visible = preferred_entrypoints
.iter()
.filter(|tool| visible_tool_names.iter().any(|name| name == *tool))
.cloned()
.collect::<Vec<_>>();
let recommended_entrypoint = preferred_entrypoints_visible.first().cloned();
Ok((
json!({
"activated": true,
"project": activate_payload,
"active_surface": active_surface.as_label(),
"token_budget": token_budget,
"config": config_payload,
"capabilities": capabilities_payload,
"warnings": warnings,
"http_session": build_http_session_payload(state, &request),
"visible_tools": {
"tool_count": visible.tools.len(),
"tool_count_total": visible.total_tool_count,
"tool_names": visible_tool_names,
"all_namespaces": visible.all_namespaces,
"all_tiers": visible.all_tiers,
"preferred_namespaces": visible.preferred_namespaces,
"preferred_tiers": visible.preferred_tiers,
"loaded_namespaces": visible.loaded_namespaces,
"loaded_tiers": visible.loaded_tiers,
"effective_namespaces": visible.effective_namespaces,
"effective_tiers": visible.effective_tiers,
"selected_namespace": visible.selected_namespace,
"selected_tier": visible.selected_tier,
"deferred_loading_active": visible.deferred_loading_active,
"full_tool_exposure": visible.full_tool_exposure,
},
"routing": {
"preferred_entrypoints": preferred_entrypoints,
"preferred_entrypoints_source": preferred_entrypoints_source,
"preferred_entrypoints_visible": preferred_entrypoints_visible,
"recommended_entrypoint": recommended_entrypoint,
},
"harness": {
"effort_level": state.effort_level().as_str(),
"compression_offset": state.effort_level().compression_threshold_offset(),
"meta_max_result_size": true,
"rapid_burst_detection": true,
"schema_pre_validation": true,
"doom_loop_threshold": 3,
"preflight_ttl_seconds": state.preflight_ttl_seconds(),
}
}),
success_meta(BackendKind::Session, 1.0),
))
}
pub fn onboarding(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
let force = arguments
.get("force")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !force {
let existing = list_memory_names(&state.memories_dir(), None);
let required = [
"project_overview",
"style_and_conventions",
"suggested_commands",
"task_completion",
];
if required.iter().all(|r| existing.contains(&r.to_string())) {
return Ok((
json!({"status":"already_onboarded","existing_memories": existing}),
success_meta(BackendKind::Session, 1.0),
));
}
}
let memories_dir = state.memories_dir();
std::fs::create_dir_all(&memories_dir)?;
let project = state.project();
let project_name = project
.as_path()
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let defaults = [
(
"project_overview",
format!(
"# Project: {project_name}\nBase path: {}\n",
project.as_path().display()
),
),
(
"style_and_conventions",
"# Style & Conventions\nTo be filled during onboarding.".to_string(),
),
(
"suggested_commands",
"# Suggested Commands\n- cargo build\n- cargo test".to_string(),
),
(
"task_completion",
"# Task Completion Checklist\n- Build passes\n- Tests pass\n- No regressions"
.to_string(),
),
];
for (name, content) in &defaults {
let path = memories_dir.join(format!("{name}.md"));
if !path.exists() {
std::fs::write(&path, content)?;
}
}
let created = list_memory_names(&state.memories_dir(), None);
Ok((
json!({"status":"onboarded","project_name": project_name,"memories_created": created}),
success_meta(BackendKind::Session, 1.0),
))
}
pub fn prepare_for_new_conversation(
state: &AppState,
_arguments: &serde_json::Value,
) -> ToolResult {
let project = state.project();
let project_name = project
.as_path()
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
Ok((
json!({
"status":"ready",
"project_name": project_name,
"project_base_path": project.as_path().to_string_lossy(),
"backend_id": "rust-core",
"memory_count": list_memory_names(&state.memories_dir(), None).len()
}),
success_meta(BackendKind::Session, 1.0),
))
}
pub fn summarize_changes(state: &AppState, _arguments: &serde_json::Value) -> ToolResult {
Ok((
json!({
"instructions": "To summarize your changes:\n1. Use search_for_pattern to identify modified symbols\n2. Use get_symbols_overview to understand file structure\n3. Write a summary to memory using write_memory with name 'session_summary'",
"project_name": state.project().as_path().file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default()
}),
success_meta(BackendKind::Session, 1.0),
))
}
pub fn list_queryable_projects(state: &AppState, _arguments: &serde_json::Value) -> ToolResult {
let project = state.project();
let project_name = project
.as_path()
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let has_memories = state.memories_dir().is_dir();
let mut projects = vec![json!({
"name": project_name,
"path": project.as_path().to_string_lossy(),
"is_active": true,
"has_memories": has_memories
})];
for (name, path) in state.list_secondary_projects() {
projects.push(json!({
"name": name,
"path": path,
"is_active": false,
"has_memories": false
}));
}
let count = projects.len();
Ok((
json!({ "projects": projects, "count": count }),
success_meta(BackendKind::Session, 1.0),
))
}
pub fn add_queryable_project(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
let path = required_string(arguments, "path")?;
match state.add_secondary_project(path) {
Ok(name) => Ok((
json!({ "added": true, "name": name, "path": path }),
success_meta(BackendKind::Session, 1.0),
)),
Err(e) => Err(crate::error::CodeLensError::NotFound(e.to_string())),
}
}
pub fn remove_queryable_project(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
let name = required_string(arguments, "name")?;
let removed = state.remove_secondary_project(name);
Ok((
json!({ "removed": removed, "name": name }),
success_meta(BackendKind::Session, 1.0),
))
}
pub fn query_project(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
let project_name = required_string(arguments, "project_name")?;
let symbol_name = required_string(arguments, "symbol_name")?;
let max_results = arguments
.get("max_results")
.and_then(|v| v.as_u64())
.unwrap_or(20) as usize;
let symbols = state
.query_secondary_project(project_name, symbol_name, max_results)
.map_err(|e| crate::error::CodeLensError::NotFound(e.to_string()))?;
Ok((
json!({
"project": project_name,
"symbols": symbols,
"count": symbols.len()
}),
success_meta(BackendKind::TreeSitter, 0.90),
))
}
pub fn auto_set_embed_hint_lang(project_path: &std::path::Path) {
let auto_hint_gate_enabled = std::env::var("CODELENS_EMBED_HINT_AUTO")
.ok()
.map(|v| {
let lowered = v.trim().to_ascii_lowercase();
match lowered.as_str() {
"1" | "true" | "yes" | "on" => true,
"0" | "false" | "no" | "off" => false,
_ => true, }
})
.unwrap_or(true);
let user_forced_lang = std::env::var("CODELENS_EMBED_HINT_AUTO_LANG").is_ok();
if !auto_hint_gate_enabled || user_forced_lang {
return;
}
let Some(lang) = compute_dominant_language(project_path) else {
return;
};
unsafe {
std::env::set_var("CODELENS_EMBED_HINT_AUTO_LANG", lang);
}
}