use std::path::{Path, PathBuf};
use context_core::cache::{CacheManifest, ContextCache};
use context_core::selection::ContextSelector;
use context_core::types::Query;
use crate::config::ServerConfig;
use crate::protocol::{McpErrorCode, McpErrorResponse, ResolveContextParams, ToolResult};
pub async fn handle(
params: ResolveContextParams,
config: &ServerConfig,
) -> ToolResult {
if params.budget < 0 {
return McpErrorResponse::canonical(McpErrorCode::InvalidBudget).into();
}
let budget = params.budget as usize;
let cache_path = match resolve_cache_path(&config.cache_root, ¶ms.cache) {
Ok(p) => p,
Err(err) => return err.into(),
};
let timeout = config.tool_timeout;
let task = tokio::task::spawn_blocking(move || {
load_and_select(&cache_path, ¶ms.query, budget)
});
match tokio::time::timeout(timeout, task).await {
Ok(Ok(Ok(selection_json))) => ToolResult::text(selection_json),
Ok(Ok(Err(mcp_err))) => mcp_err.into(),
Ok(Err(join_err)) => {
eprintln!("Task join error: {join_err}");
McpErrorResponse::canonical(McpErrorCode::InternalError).into()
}
Err(_) => {
eprintln!("Operation timed out after {} seconds", timeout.as_secs());
McpErrorResponse::canonical(McpErrorCode::InternalError).into()
}
}
}
fn load_and_select(
cache_path: &Path,
query_str: &str,
budget: usize,
) -> Result<String, McpErrorResponse> {
let manifest_path = cache_path.join("manifest.json");
let manifest_file = std::fs::File::open(&manifest_path).map_err(|e| {
eprintln!("Cannot read manifest: {e}");
if e.kind() == std::io::ErrorKind::NotFound {
McpErrorResponse::canonical(McpErrorCode::CacheInvalid)
} else {
McpErrorResponse::canonical(McpErrorCode::IoError)
}
})?;
let manifest: CacheManifest = serde_json::from_reader(manifest_file).map_err(|e| {
eprintln!("Invalid manifest JSON: {e}");
McpErrorResponse::canonical(McpErrorCode::CacheInvalid)
})?;
let cache = ContextCache {
root: cache_path.to_path_buf(),
manifest,
};
let selector = ContextSelector::default();
let query = Query::new(query_str);
let selection = selector.select(&cache, query, budget).map_err(|e| {
eprintln!("Selection failed: {e}");
McpErrorResponse::canonical(McpErrorCode::InternalError)
})?;
let json = serde_json::to_string(&selection).map_err(|e| {
eprintln!("Serialization failed: {e}");
McpErrorResponse::canonical(McpErrorCode::InternalError)
})?;
Ok(format!("{json}\n"))
}
fn resolve_cache_path(cache_root: &Path, cache_name: &str) -> Result<PathBuf, McpErrorResponse> {
if cache_name.contains("..") || cache_name.starts_with('/') || cache_name.starts_with('\\') {
return Err(McpErrorResponse::canonical(McpErrorCode::CacheMissing));
}
let candidate = cache_root.join(cache_name);
let canonical = candidate.canonicalize().map_err(|_| {
McpErrorResponse::canonical(McpErrorCode::CacheMissing)
})?;
let root_canonical = cache_root.canonicalize().map_err(|e| {
eprintln!("Cache root not accessible: {e}");
McpErrorResponse::canonical(McpErrorCode::IoError)
})?;
if !canonical.starts_with(&root_canonical) {
return Err(McpErrorResponse::canonical(McpErrorCode::CacheMissing));
}
if !canonical.is_dir() {
return Err(McpErrorResponse::canonical(McpErrorCode::CacheMissing));
}
Ok(canonical)
}