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;