bookmark 0.1.3

Import, search, and open bookmarks from all browsers
Documentation
mod tools;

use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::io::{self, BufRead, Write};

#[derive(Debug, Serialize, Deserialize)]
struct McpRequest {
    jsonrpc: String,
    id: Option<Value>,
    method: String,
    params: Option<Value>,
}

#[derive(Debug, Serialize, Deserialize)]
struct McpResponse {
    jsonrpc: String,
    id: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    result: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<McpError>,
}

#[derive(Debug, Serialize, Deserialize)]
struct McpError {
    code: i32,
    message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    data: Option<Value>,
}

pub struct McpServer {
    name: String,
    version: String,
}

impl McpServer {
    pub fn new() -> Self {
        Self {
            name: "bookmark-mcp".to_string(),
            version: env!("CARGO_PKG_VERSION").to_string(),
        }
    }

    pub fn run(&self) -> Result<()> {
        log::info!("Starting MCP server: {} v{}", self.name, self.version);

        let stdin = io::stdin();
        let mut stdout = io::stdout();

        for line in stdin.lock().lines() {
            let line = line?;
            if line.trim().is_empty() {
                continue;
            }

            let response = match serde_json::from_str::<McpRequest>(&line) {
                Ok(request) => self.handle_request(request),
                Err(e) => McpResponse {
                    jsonrpc: "2.0".to_string(),
                    id: None,
                    result: None,
                    error: Some(McpError {
                        code: -32700,
                        message: format!("Parse error: {}", e),
                        data: None,
                    }),
                },
            };

            let response_json = serde_json::to_string(&response)?;
            writeln!(stdout, "{}", response_json)?;
            stdout.flush()?;
        }

        Ok(())
    }

    fn handle_request(&self, request: McpRequest) -> McpResponse {
        log::debug!("Handling request: {:?}", request.method);

        let result = match request.method.as_str() {
            "initialize" => self.handle_initialize(request.params),
            "tools/list" => self.handle_list_tools(),
            "tools/call" => self.handle_tool_call(request.params),
            _ => Err(anyhow::anyhow!("Method not found: {}", request.method)),
        };

        match result {
            Ok(value) => McpResponse {
                jsonrpc: "2.0".to_string(),
                id: request.id,
                result: Some(value),
                error: None,
            },
            Err(e) => McpResponse {
                jsonrpc: "2.0".to_string(),
                id: request.id,
                result: None,
                error: Some(McpError {
                    code: -32603,
                    message: e.to_string(),
                    data: None,
                }),
            },
        }
    }

    fn handle_initialize(&self, _params: Option<Value>) -> Result<Value> {
        Ok(json!({
            "protocolVersion": "2024-11-05",
            "capabilities": {
                "tools": {}
            },
            "serverInfo": {
                "name": self.name,
                "version": self.version
            }
        }))
    }

    fn handle_list_tools(&self) -> Result<Value> {
        Ok(json!({
            "tools": [
                {
                    "name": "export_bookmarks",
                    "description": "Export bookmarks from a browser",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "browser": {
                                "type": "string",
                                "description": "Browser name (chrome, firefox, safari, edge, all)",
                                "enum": ["chrome", "firefox", "safari", "edge", "all"]
                            },
                            "data_type": {
                                "type": "string",
                                "description": "Data type to export",
                                "enum": ["bookmarks", "history", "both"],
                                "default": "bookmarks"
                            }
                        },
                        "required": ["browser"]
                    }
                },
                {
                    "name": "search_bookmarks",
                    "description": "Search bookmarks by query",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "query": {
                                "type": "string",
                                "description": "Search query"
                            },
                            "title_only": {
                                "type": "boolean",
                                "description": "Search in title only",
                                "default": false
                            },
                            "url_only": {
                                "type": "boolean",
                                "description": "Search in URL only",
                                "default": false
                            },
                            "limit": {
                                "type": "integer",
                                "description": "Maximum number of results",
                                "default": 20
                            }
                        },
                        "required": ["query"]
                    }
                },
                {
                    "name": "list_browsers",
                    "description": "List available browsers and their profiles",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "browser": {
                                "type": "string",
                                "description": "Specific browser to list (optional)",
                                "enum": ["chrome", "firefox", "safari", "edge"]
                            }
                        }
                    }
                },
                {
                    "name": "process_bookmarks",
                    "description": "Deduplicate and organize bookmarks",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "bookmarks": {
                                "type": "array",
                                "description": "Array of bookmarks to process"
                            },
                            "mode": {
                                "type": "string",
                                "description": "Processing mode",
                                "enum": ["dedupe", "organize", "both"],
                                "default": "both"
                            },
                            "strategy": {
                                "type": "string",
                                "description": "Merge strategy for duplicates",
                                "enum": ["first", "last", "recent", "merge"],
                                "default": "merge"
                            }
                        },
                        "required": ["bookmarks"]
                    }
                },
                {
                    "name": "generate_graph",
                    "description": "Generate knowledge graph from bookmarks",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "browser": {
                                "type": "string",
                                "description": "Browser source",
                                "enum": ["chrome", "firefox", "safari", "edge", "all"],
                                "default": "all"
                            },
                            "format": {
                                "type": "string",
                                "description": "Output format",
                                "enum": ["dot", "json", "gexf"],
                                "default": "json"
                            },
                            "min_threshold": {
                                "type": "integer",
                                "description": "Minimum bookmarks for domain node",
                                "default": 2
                            }
                        }
                    }
                }
            ]
        }))
    }

    fn handle_tool_call(&self, params: Option<Value>) -> Result<Value> {
        let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
        let tool_name = params["name"]
            .as_str()
            .ok_or_else(|| anyhow::anyhow!("Missing tool name"))?;
        let arguments = params["arguments"].clone();

        match tool_name {
            "export_bookmarks" => self.tool_export_bookmarks(arguments),
            "search_bookmarks" => self.tool_search_bookmarks(arguments),
            "list_browsers" => self.tool_list_browsers(arguments),
            "process_bookmarks" => self.tool_process_bookmarks(arguments),
            "generate_graph" => self.tool_generate_graph(arguments),
            _ => Err(anyhow::anyhow!("Unknown tool: {}", tool_name)),
        }
    }
}

impl Default for McpServer {
    fn default() -> Self {
        Self::new()
    }
}