kagi-mcp 1.0.2

MCP stdio server for kagi-sdk
Documentation
#![forbid(unsafe_code)]

mod backend;
mod error;
pub mod normalize;
mod schema;

use backend::BackendRuntime;
pub use backend::{ENV_API_KEY, ENV_BACKEND_MODE, ENV_SESSION_TOKEN};
pub use error::StartupError;
use rmcp::{
    handler::server::{router::tool::ToolRouter, wrapper::Json, wrapper::Parameters},
    model::{ServerCapabilities, ServerInfo},
    tool, tool_handler, tool_router, ServerHandler, ServiceExt,
};
pub use schema::{SearchResultCard, SearchToolOutput, SummarizeToolOutput};

#[derive(Debug, Clone)]
pub struct KagiMcpServer {
    backend: BackendRuntime,
    tool_router: ToolRouter<Self>,
}

impl KagiMcpServer {
    pub fn from_env() -> Result<Self, StartupError> {
        Self::from_backend(BackendRuntime::from_process_env(
            kagi_sdk::ClientConfig::default(),
        )?)
    }

    fn from_backend(backend: BackendRuntime) -> Result<Self, StartupError> {
        Ok(Self {
            backend,
            tool_router: Self::tool_router(),
        })
    }

    pub async fn serve_stdio(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let running = self.serve(rmcp::transport::stdio()).await?;
        let _ = running.waiting().await?;
        Ok(())
    }
}

#[tool_router]
impl KagiMcpServer {
    #[tool(
        name = "kagi_search",
        description = "Search Kagi and return normalized result cards.",
        annotations(read_only_hint = true, idempotent_hint = true)
    )]
    async fn kagi_search(
        &self,
        Parameters(input): Parameters<schema::SearchToolInput>,
    ) -> Result<Json<schema::SearchToolOutput>, String> {
        self.backend
            .search(&input)
            .await
            .map(Json)
            .map_err(|error| error.message().to_string())
    }

    #[tool(
        name = "kagi_summarize",
        description = "Summarize a URL or raw text with Kagi.",
        annotations(read_only_hint = true, idempotent_hint = true)
    )]
    async fn kagi_summarize(
        &self,
        Parameters(input): Parameters<schema::SummarizeToolInput>,
    ) -> Result<Json<schema::SummarizeToolOutput>, String> {
        self.backend
            .summarize(&input)
            .await
            .map(Json)
            .map_err(|error| error.message().to_string())
    }
}

#[tool_handler(router = self.tool_router)]
impl ServerHandler for KagiMcpServer {
    fn get_info(&self) -> ServerInfo {
        ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
    }
}

#[cfg(test)]
mod tests;