use rust_mcp_sdk::schema::{
CompleteRequestParams, CompleteRequestRef, CompleteResult, CompleteResultCompletion,
};
const ALL_BROWSERS: &[&str] = &[
"brave", "chrome", "firefox", "safari", "edge", "auto", "none",
];
const FINGERPRINT_PROFILES: &[&str] = &[
"chrome-131-macos",
"chrome-131-windows",
"chrome-131-linux",
"firefox-133-macos",
"firefox-133-windows",
"safari-18-macos",
"edge-131-windows",
"random",
];
pub(crate) fn handle_complete(params: &CompleteRequestParams) -> CompleteResult {
let suggestions = match ¶ms.ref_ {
CompleteRequestRef::PromptReference(prompt_ref) => complete_prompt_arg(
&prompt_ref.name,
¶ms.argument.name,
¶ms.argument.value,
),
CompleteRequestRef::ResourceTemplateReference(_) => vec![],
};
build_complete_result(suggestions)
}
fn complete_prompt_arg(prompt_name: &str, arg_name: &str, prefix: &str) -> Vec<String> {
match arg_name {
"auth_method" | "cookies" => filter_prefix(ALL_BROWSERS, prefix),
"browser" => filter_prefix(FINGERPRINT_PROFILES, prefix),
"session" => complete_session_names(prefix),
"url" | "urls" => complete_url(prompt_name, prefix),
_ => vec![],
}
}
fn filter_prefix(candidates: &[&str], prefix: &str) -> Vec<String> {
let lower = prefix.to_ascii_lowercase();
candidates
.iter()
.filter(|c| c.to_ascii_lowercase().starts_with(lower.as_str()))
.map(|s| (*s).to_string())
.collect()
}
fn complete_session_names(prefix: &str) -> Vec<String> {
let Ok(session_dir) = nab::get_session_dir() else {
return vec![];
};
let Ok(entries) = std::fs::read_dir(&session_dir) else {
return vec![];
};
entries
.filter_map(|entry| {
let entry = entry.ok()?;
let name = entry.file_name().into_string().ok()?;
let session_name = name.strip_suffix(".json").unwrap_or(&name);
if session_name.starts_with(prefix) {
Some(session_name.to_string())
} else {
None
}
})
.collect()
}
#[allow(unused_variables)]
fn complete_url(prompt_name: &str, prefix: &str) -> Vec<String> {
vec![]
}
fn build_complete_result(values: Vec<String>) -> CompleteResult {
let has_more = false; CompleteResult {
completion: CompleteResultCompletion {
values,
total: None,
has_more: if has_more { Some(true) } else { None },
},
meta: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filter_prefix_returns_matching_browsers() {
let results = filter_prefix(ALL_BROWSERS, "ch");
assert_eq!(results, vec!["chrome"]);
}
#[test]
fn filter_prefix_empty_prefix_returns_all() {
let results = filter_prefix(ALL_BROWSERS, "");
assert_eq!(results.len(), ALL_BROWSERS.len());
}
#[test]
fn filter_prefix_case_insensitive() {
let results = filter_prefix(ALL_BROWSERS, "BR");
assert_eq!(results, vec!["brave"]);
}
#[test]
fn filter_prefix_no_match_returns_empty() {
let results = filter_prefix(ALL_BROWSERS, "zzz");
assert!(results.is_empty());
}
#[test]
fn cookies_arg_completes_from_browser_list() {
let results = complete_prompt_arg("fetch-and-extract", "cookies", "f");
assert!(results.contains(&"firefox".to_string()));
assert!(!results.contains(&"brave".to_string()));
}
#[test]
fn browser_arg_completes_from_fingerprint_profiles() {
let results = complete_prompt_arg("fetch-and-extract", "browser", "chrome");
assert!(!results.is_empty());
assert!(results.iter().all(|s| s.starts_with("chrome")));
}
#[test]
fn unknown_arg_returns_empty() {
let results = complete_prompt_arg("some-prompt", "unknown_arg", "");
assert!(results.is_empty());
}
#[test]
fn handle_complete_prompt_cookies_returns_browsers() {
let params = CompleteRequestParams {
argument: rust_mcp_sdk::schema::CompleteRequestArgument {
name: "cookies".to_string(),
value: String::new(),
},
ref_: CompleteRequestRef::PromptReference(rust_mcp_sdk::schema::PromptReference::new(
"fetch-and-extract".to_string(),
None,
)),
context: None,
meta: None,
};
let result = handle_complete(¶ms);
assert!(!result.completion.values.is_empty());
assert!(result.completion.values.contains(&"brave".to_string()));
assert!(result.completion.values.contains(&"chrome".to_string()));
}
#[test]
fn handle_complete_resource_template_returns_empty() {
let params = CompleteRequestParams {
argument: rust_mcp_sdk::schema::CompleteRequestArgument {
name: "uri".to_string(),
value: "nab://".to_string(),
},
ref_: CompleteRequestRef::ResourceTemplateReference(
rust_mcp_sdk::schema::ResourceTemplateReference::new(
"nab://watch/{id}".to_string(),
),
),
context: None,
meta: None,
};
let result = handle_complete(¶ms);
assert!(result.completion.values.is_empty());
}
#[test]
fn build_complete_result_has_correct_shape() {
let values = vec!["brave".to_string(), "chrome".to_string()];
let result = build_complete_result(values.clone());
assert_eq!(result.completion.values, values);
assert!(result.completion.has_more.is_none());
}
}