use std::path::Path;
use tokensave::tokensave::TokenSave;
pub async fn ensure_initialized(project_path: &Path) -> tokensave::errors::Result<TokenSave> {
if TokenSave::is_initialized(project_path) {
return TokenSave::open(project_path).await;
}
Err(tokensave::errors::TokenSaveError::Config {
message: format!(
"no TokenSave index found at '{}' — run 'tokensave init' first",
project_path.display()
),
})
}
pub async fn resolve_serve_from_global_db() -> Option<std::path::PathBuf> {
let gdb = tokensave::global_db::GlobalDb::open().await?;
let mut paths: Vec<String> = gdb.list_project_paths().await;
paths.retain(|p| {
std::path::Path::new(p)
.join(".tokensave/tokensave.db")
.exists()
});
if paths.len() == 1 {
return Some(std::path::PathBuf::from(paths.remove(0)));
}
if paths.is_empty() {
return None;
}
let cwd = std::env::current_dir().ok()?;
let cwd = cwd.canonicalize().unwrap_or(cwd);
let mut ancestors: Vec<_> = paths
.iter()
.filter_map(|p| {
let pp = std::path::Path::new(p).canonicalize().ok()?;
cwd.starts_with(&pp)
.then(|| (pp.components().count(), p.clone()))
})
.collect();
ancestors.sort_by_key(|a| std::cmp::Reverse(a.0)); if let Some((_, best)) = ancestors.into_iter().next() {
return Some(std::path::PathBuf::from(best));
}
let mut descendants: Vec<_> = paths
.iter()
.filter_map(|p| {
let pp = std::path::Path::new(p).canonicalize().ok()?;
pp.starts_with(&cwd)
.then(|| (pp.components().count(), p.clone()))
})
.collect();
descendants.sort_by_key(|a| a.0); if let Some((_, best)) = descendants.into_iter().next() {
return Some(std::path::PathBuf::from(best));
}
eprintln!("Multiple tokensave projects found — pass -p <path> to select one:");
for p in &paths {
eprintln!(" {p}");
}
None
}
pub async fn resolve_serve_from_mcp_roots(out: &mut Option<String>) -> Option<std::path::PathBuf> {
use tokio::io::AsyncBufReadExt;
let stdin = tokio::io::stdin();
let mut reader = tokio::io::BufReader::new(stdin);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => return None, Ok(_) => {
let trimmed = line.trim();
if !trimmed.is_empty() {
break;
}
}
Err(_) => return None,
}
}
*out = Some(line.trim().to_string());
let parsed: serde_json::Value = serde_json::from_str(line.trim()).ok()?;
let roots = parsed.pointer("/params/roots").and_then(|v| v.as_array())?;
let gdb = tokensave::global_db::GlobalDb::open().await?;
let registered: Vec<RegisteredProject> = gdb
.list_project_paths()
.await
.into_iter()
.filter_map(|p| {
let pb = std::path::PathBuf::from(p);
if pb.join(".tokensave/tokensave.db").exists() {
let canonical = pb.canonicalize().ok();
Some(RegisteredProject {
path: pb,
canonical,
})
} else {
None
}
})
.collect();
for root in roots {
let uri = root.get("uri").and_then(|v| v.as_str()).unwrap_or_default();
let Some(root_path) = root_uri_to_path(uri) else {
tracing::debug!("skipping unsupported root URI: {}", uri);
continue;
};
if let Some(hit) = find_registered_project(®istered, &root_path) {
tracing::info!("serving from MCP root: {}", hit.display());
return Some(hit);
}
if let Some(discovered) = tokensave::config::discover_project_root(&root_path) {
tracing::info!(
"serving from discovered project under MCP root: {}",
discovered.display()
);
return Some(discovered);
}
}
None
}
struct RegisteredProject {
path: std::path::PathBuf,
canonical: Option<std::path::PathBuf>,
}
fn find_registered_project(
registered: &[RegisteredProject],
root_path: &std::path::Path,
) -> Option<std::path::PathBuf> {
let canonical_root = root_path.canonicalize().ok();
registered
.iter()
.find(|rp| {
rp.path == root_path
|| match (rp.canonical.as_ref(), canonical_root.as_ref()) {
(Some(a), Some(b)) => a == b,
_ => false,
}
})
.map(|rp| rp.path.clone())
}
fn root_uri_to_path(uri: &str) -> Option<std::path::PathBuf> {
root_uri_to_path_on(uri, cfg!(windows))
}
fn root_uri_to_path_on(uri: &str, strip_drive_letter: bool) -> Option<std::path::PathBuf> {
if uri.is_empty() {
return None;
}
let path = match uri.strip_prefix("file://") {
Some(rest) if authority_is_localhost(rest) => {
let authority_len = rest.split('/').next().unwrap_or_default().len();
percent_decode(&rest[authority_len..])?
}
Some(rest) if !rest.starts_with('/') => return None,
Some(rest) => percent_decode(rest)?,
None if uri.contains("://") => return None,
None => uri.to_string(),
};
if path.is_empty() {
return None;
}
let path = match path.strip_prefix('/') {
Some(rest) if strip_drive_letter && has_drive_prefix(rest) => rest.to_string(),
_ => path,
};
Some(std::path::PathBuf::from(path))
}
fn authority_is_localhost(rest: &str) -> bool {
rest.split('/').next().is_some_and(|authority| {
authority.eq_ignore_ascii_case("localhost")
|| authority == "127.0.0.1"
|| authority == "[::1]"
})
}
fn has_drive_prefix(path: &str) -> bool {
let mut chars = path.chars();
matches!((chars.next(), chars.next()), (Some(c), Some(':')) if c.is_ascii_alphabetic())
}
fn percent_decode(input: &str) -> Option<String> {
let bytes = input.as_bytes();
if !bytes.contains(&b'%') {
return Some(input.to_string());
}
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let (Some(hi), Some(lo)) = (
(bytes[i + 1] as char).to_digit(16),
(bytes[i + 2] as char).to_digit(16),
) {
out.push((hi * 16 + lo) as u8);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8(out).ok()
}
#[cfg(test)]
mod tests {
use super::{
find_registered_project, has_drive_prefix, percent_decode, root_uri_to_path,
root_uri_to_path_on,
};
use std::path::PathBuf;
fn win(uri: &str) -> String {
render(root_uri_to_path_on(uri, true))
}
fn unix(uri: &str) -> String {
render(root_uri_to_path_on(uri, false))
}
fn render(path: Option<PathBuf>) -> String {
path.unwrap_or_else(|| PathBuf::from("<none>"))
.to_string_lossy()
.replace('\\', "/")
}
#[test]
fn windows_root_drops_the_uri_leading_slash() {
assert_eq!(win("file:///D:/Dev/app"), "D:/Dev/app");
assert_eq!(win("file:///c:/dev/app"), "c:/dev/app");
}
#[test]
fn a_drive_letter_is_only_a_drive_letter_on_windows() {
assert_eq!(unix("file:///C:/x"), "/C:/x");
}
#[test]
fn windows_keeps_the_leading_slash_without_a_drive_prefix() {
assert_eq!(win("file:///home/user/app"), "/home/user/app");
assert_eq!(win("file:///d/dev/app"), "/d/dev/app");
}
#[test]
fn localhost_authority_is_the_local_machine() {
assert_eq!(unix("file://localhost/home/user/app"), "/home/user/app");
assert_eq!(win("file://localhost/D:/Dev/app"), "D:/Dev/app");
assert_eq!(unix("file://LOCALHOST/home/user/app"), "/home/user/app");
assert_eq!(unix("file://LocalHost/home/user/app"), "/home/user/app");
assert!(root_uri_to_path("file://localhostname/share").is_none());
}
#[test]
fn ip_literal_authorities_are_the_local_machine() {
assert_eq!(unix("file://127.0.0.1/home/user/app"), "/home/user/app");
assert_eq!(win("file://127.0.0.1/D:/Dev/app"), "D:/Dev/app");
assert_eq!(unix("file://[::1]/home/user/app"), "/home/user/app");
assert_eq!(win("file://[::1]/D:/Dev/app"), "D:/Dev/app");
assert_eq!(unix("file://[::1]/x"), "/x");
assert!(root_uri_to_path("file://127.0.0.1.evil.com/x").is_none());
}
#[test]
fn a_multibyte_authority_is_rejected_without_panicking() {
assert!(root_uri_to_path("file://localhosé/share").is_none());
assert!(root_uri_to_path("file://locälhost/x").is_none());
assert!(root_uri_to_path("file://é/x").is_none());
}
#[test]
fn percent_escapes_are_decoded() {
assert_eq!(win("file:///C:/My%20Project"), "C:/My Project");
assert_eq!(win("file:///home/user/my%20app"), "/home/user/my app");
assert_eq!(win("file:///home/user/caf%C3%A9"), "/home/user/café");
}
#[test]
fn malformed_escapes_are_left_alone() {
assert_eq!(percent_decode("100%"), Some("100%".to_string()));
assert_eq!(percent_decode("a%zz"), Some("a%zz".to_string()));
assert_eq!(percent_decode("a%4"), Some("a%4".to_string()));
assert_eq!(percent_decode("%41"), Some("A".to_string()));
}
#[test]
fn escapes_decoding_to_invalid_utf8_reject_the_root() {
assert!(root_uri_to_path("file:///tmp/%C3").is_none());
assert!(root_uri_to_path("file:///tmp/%FF").is_none());
assert!(root_uri_to_path("file:///tmp/%E2%82").is_none());
assert!(root_uri_to_path("file://localhost/%FF").is_none());
assert_eq!(percent_decode("caf%C3%A9"), Some("café".to_string()));
}
#[test]
fn a_localhost_authority_without_a_path_is_rejected() {
assert!(root_uri_to_path("file://localhost").is_none());
assert!(root_uri_to_path("file://[::1]").is_none());
}
#[test]
fn bare_paths_pass_through() {
assert_eq!(win("/home/user/app"), "/home/user/app");
assert_eq!(win("D:/Dev/app"), "D:/Dev/app");
assert!(root_uri_to_path("/home/user/app").is_some());
}
#[test]
fn unsupported_uris_are_skipped() {
assert!(root_uri_to_path("").is_none());
assert!(root_uri_to_path("https://example.com/app").is_none());
assert!(root_uri_to_path("file://server/share").is_none());
}
#[test]
fn drive_prefix_detection() {
assert!(has_drive_prefix("C:/x"));
assert!(has_drive_prefix("z:"));
assert!(!has_drive_prefix("/C:/x"));
assert!(!has_drive_prefix("home/user"));
assert!(!has_drive_prefix("1:/x"));
assert!(!has_drive_prefix(""));
}
#[test]
fn registered_match_literal_even_when_the_path_does_not_exist() {
let tmp = tempfile::TempDir::new().unwrap();
let missing = tmp.path().join("missing");
assert!(!missing.exists());
let registered = vec![missing.to_string_lossy().into_owned()];
assert_eq!(
find_registered_project(&prep(registered), &missing),
Some(missing)
);
}
#[test]
fn registered_match_via_canonicalization_returns_the_registered_spelling() {
let tmp = tempfile::TempDir::new().unwrap();
let project = tmp.path().join("project");
std::fs::create_dir(&project).unwrap();
let registered = vec![project.to_string_lossy().into_owned()];
let alias = project.join("..").join("project");
assert_ne!(alias.as_path(), project.as_path());
assert_eq!(
find_registered_project(&prep(registered), &alias),
Some(project.clone())
);
}
fn prep(registered: Vec<String>) -> Vec<super::RegisteredProject> {
registered
.into_iter()
.map(|p| {
let pb = PathBuf::from(p);
let canonical = pb.canonicalize().ok();
super::RegisteredProject {
path: pb,
canonical,
}
})
.collect()
}
#[test]
fn registered_mismatch_between_two_existing_directories() {
let tmp = tempfile::TempDir::new().unwrap();
let a = tmp.path().join("a");
let b = tmp.path().join("b");
std::fs::create_dir(&a).unwrap();
std::fs::create_dir(&b).unwrap();
let registered = vec![a.to_string_lossy().into_owned()];
assert_eq!(find_registered_project(&prep(registered), &b), None);
}
}