bookmark 0.1.3

Import, search, and open bookmarks from all browsers
Documentation
use anyhow::Result;
use serde_json::{json, Value};
use std::path::PathBuf;

use crate::browser::Browser;
use crate::exporter::{export_data, Bookmark};
use crate::graph::{GraphBuilder, GraphConfig};
use crate::processor::{BookmarkProcessor, ProcessingConfig};
use crate::search::{search_bookmarks_internal, SearchOptions};

use super::McpServer;

impl McpServer {
    pub(super) fn tool_export_bookmarks(&self, args: Value) -> Result<Value> {
        let browser = args["browser"]
            .as_str()
            .ok_or_else(|| anyhow::anyhow!("Missing browser"))?;
        let data_type = args["data_type"].as_str().unwrap_or("bookmarks");

        let temp_dir = PathBuf::from("/tmp/bookmark_mcp");
        std::fs::create_dir_all(&temp_dir)?;

        let output_file = temp_dir.join(format!("{}-{}.yaml", browser, data_type));
        export_data(browser, data_type, Some(output_file.clone()), None)?;

        let content = std::fs::read_to_string(&output_file)?;
        let data: Vec<crate::exporter::BrowserData> = serde_yaml::from_str(&content)?;

        Ok(json!({
            "content": [{
                "type": "text",
                "text": serde_json::to_string_pretty(&data)?
            }]
        }))
    }

    pub(super) fn tool_search_bookmarks(&self, args: Value) -> Result<Value> {
        let query = args["query"]
            .as_str()
            .ok_or_else(|| anyhow::anyhow!("Missing query"))?;
        let title_only = args["title_only"].as_bool().unwrap_or(false);
        let url_only = args["url_only"].as_bool().unwrap_or(false);
        let limit = args["limit"].as_u64().unwrap_or(20) as usize;

        let options = SearchOptions {
            title_only,
            url_only,
            limit,
        };

        let results = search_bookmarks_internal(query, &options)?;

        let formatted_results: Vec<String> = results
            .iter()
            .map(|b| format!("{} - {}", b.title, b.url.as_deref().unwrap_or("N/A")))
            .collect();

        Ok(json!({
            "content": [{
                "type": "text",
                "text": formatted_results.join("\n")
            }]
        }))
    }

    pub(super) fn tool_list_browsers(&self, args: Value) -> Result<Value> {
        let mut output = Vec::new();

        let browsers = if let Some(browser_name) = args["browser"].as_str() {
            vec![browser_name]
        } else {
            vec!["chrome", "firefox", "safari", "edge"]
        };

        for browser_name in browsers {
            if let Ok(browser) = Browser::from_str(browser_name) {
                if let Ok(profiles) = browser.find_profiles(None) {
                    output.push(format!(
                        "{}: {} profile(s)",
                        browser_name,
                        profiles.len()
                    ));
                }
            }
        }

        Ok(json!({
            "content": [{
                "type": "text",
                "text": output.join("\n")
            }]
        }))
    }

    pub(super) fn tool_process_bookmarks(&self, args: Value) -> Result<Value> {
        let bookmarks_json = args["bookmarks"]
            .as_array()
            .ok_or_else(|| anyhow::anyhow!("Missing bookmarks array"))?;
        let mode = args["mode"].as_str().unwrap_or("both");
        let strategy = args["strategy"].as_str().unwrap_or("merge");

        let bookmarks: Vec<Bookmark> = serde_json::from_value(json!(bookmarks_json))?;

        let merge_strategy = match strategy {
            "first" => crate::deduplication::MergeStrategy::KeepFirst,
            "last" => crate::deduplication::MergeStrategy::KeepLast,
            "recent" => crate::deduplication::MergeStrategy::KeepMostRecent,
            _ => crate::deduplication::MergeStrategy::MergeMetadata,
        };

        let config = ProcessingConfig {
            deduplication_config: crate::deduplication::DeduplicationConfig {
                merge_strategy,
                normalize_urls: mode == "dedupe" || mode == "both",
                ..Default::default()
            },
            organization_config: crate::organization::OrganizationConfig::default(),
            dry_run: false,
            backup_original: false,
        };

        let processor = BookmarkProcessor::new(config);
        let result = processor.process_bookmarks(&bookmarks)?;

        Ok(json!({
            "content": [{
                "type": "text",
                "text": format!(
                    "Processed {} bookmarks:\n- Original: {}\n- Final: {}\n- Duplicates removed: {}",
                    result.processing_summary.original_count,
                    result.processing_summary.original_count,
                    result.processing_summary.final_count,
                    result.processing_summary.duplicates_removed
                )
            }],
            "processed_bookmarks": result.processed_bookmarks
        }))
    }

    pub(super) fn tool_generate_graph(&self, args: Value) -> Result<Value> {
        let browser = args["browser"].as_str().unwrap_or("all");
        let format = args["format"].as_str().unwrap_or("json");
        let min_threshold = args["min_threshold"].as_u64().unwrap_or(2) as usize;

        let (all_bookmarks, all_history) = crate::exporter::load_browser_data(browser, "both")?;

        let config = GraphConfig {
            min_domain_threshold: min_threshold,
            ..Default::default()
        };

        let mut builder = GraphBuilder::new(config);
        let graph = builder.from_both(&all_bookmarks, &all_history)?;

        let graph_content = match format {
            "dot" => crate::graph::formats::to_dot(&graph),
            "json" => crate::graph::formats::to_json(&graph),
            "gexf" => crate::graph::formats::to_gexf(&graph),
            "html" => crate::graph::formats::to_html(&graph),
            _ => return Err(anyhow::anyhow!("Invalid format: {}. Use dot, json, gexf, or html", format)),
        };

        Ok(json!({
            "content": [{
                "type": "text",
                "text": graph_content
            }],
            "metadata": {
                "nodes": graph.metadata.total_nodes,
                "edges": graph.metadata.total_edges,
                "bookmarks": graph.metadata.bookmark_count,
                "domains": graph.metadata.domain_count
            }
        }))
    }
}