Skip to main content

kagi_mcp/
lib.rs

1#![forbid(unsafe_code)]
2
3mod backend;
4mod error;
5pub mod normalize;
6mod schema;
7
8use backend::BackendRuntime;
9pub use backend::{ENV_API_KEY, ENV_BACKEND_MODE, ENV_SESSION_TOKEN};
10pub use error::StartupError;
11use rmcp::{
12    handler::server::{router::tool::ToolRouter, wrapper::Json, wrapper::Parameters},
13    model::{ServerCapabilities, ServerInfo},
14    tool, tool_handler, tool_router, ServerHandler, ServiceExt,
15};
16pub use schema::{SearchResultCard, SearchToolOutput, SummarizeToolOutput};
17
18#[derive(Debug, Clone)]
19pub struct KagiMcpServer {
20    backend: BackendRuntime,
21    tool_router: ToolRouter<Self>,
22}
23
24impl KagiMcpServer {
25    pub fn from_env() -> Result<Self, StartupError> {
26        Self::from_backend(BackendRuntime::from_process_env(
27            kagi_sdk::ClientConfig::default(),
28        )?)
29    }
30
31    fn from_backend(backend: BackendRuntime) -> Result<Self, StartupError> {
32        Ok(Self {
33            backend,
34            tool_router: Self::tool_router(),
35        })
36    }
37
38    pub async fn serve_stdio(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
39        let running = self.serve(rmcp::transport::stdio()).await?;
40        let _ = running.waiting().await?;
41        Ok(())
42    }
43}
44
45#[tool_router]
46impl KagiMcpServer {
47    #[tool(
48        name = "kagi_search",
49        description = "Search Kagi and return normalized result cards.",
50        annotations(read_only_hint = true, idempotent_hint = true)
51    )]
52    async fn kagi_search(
53        &self,
54        Parameters(input): Parameters<schema::SearchToolInput>,
55    ) -> Result<Json<schema::SearchToolOutput>, String> {
56        self.backend
57            .search(&input)
58            .await
59            .map(Json)
60            .map_err(|error| error.message().to_string())
61    }
62
63    #[tool(
64        name = "kagi_summarize",
65        description = "Summarize a URL or raw text with Kagi.",
66        annotations(read_only_hint = true, idempotent_hint = true)
67    )]
68    async fn kagi_summarize(
69        &self,
70        Parameters(input): Parameters<schema::SummarizeToolInput>,
71    ) -> Result<Json<schema::SummarizeToolOutput>, String> {
72        self.backend
73            .summarize(&input)
74            .await
75            .map(Json)
76            .map_err(|error| error.message().to_string())
77    }
78}
79
80#[tool_handler(router = self.tool_router)]
81impl ServerHandler for KagiMcpServer {
82    fn get_info(&self) -> ServerInfo {
83        ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
84    }
85}
86
87#[cfg(test)]
88mod tests;