Skip to main content

org_mcp_server/tools/
org_search.rs

1use org_core::OrgModeError;
2use rmcp::{
3    ErrorData as McpError,
4    handler::server::wrapper::Parameters,
5    model::{CallToolResult, Content, ErrorCode},
6    schemars, tool, tool_router,
7};
8
9use crate::core::OrgModeRouter;
10
11#[derive(Debug, schemars::JsonSchema, serde::Deserialize)]
12pub struct SearchRequest {
13    #[schemars(description = "Search query string to find in org file content")]
14    pub query: String,
15    #[schemars(description = "Maximum number of search results to return (optional)")]
16    pub limit: Option<usize>,
17    #[schemars(description = "Maximum snippet size in characters (optional, default: 100)")]
18    pub snippet_max_size: Option<usize>,
19    #[schemars(
20        description = "Filter results by tags (optional, matches any of the provided tags)"
21    )]
22    pub tags: Option<Vec<String>>,
23}
24
25#[tool_router(router = "tool_router_search", vis = "pub(crate)")]
26impl OrgModeRouter {
27    #[tool(
28        name = "org-search",
29        description = "Search for text content across all org files using fuzzy matching",
30        annotations(title = "org-search tool")
31    )]
32    async fn tool_search(
33        &self,
34        Parameters(SearchRequest {
35            query,
36            limit,
37            snippet_max_size,
38            tags,
39        }): Parameters<SearchRequest>,
40    ) -> Result<CallToolResult, McpError> {
41        let org_mode = self.org_mode.lock().await;
42
43        let results = if tags.is_some() {
44            org_mode.search_with_tags(&query, tags.as_deref(), limit, snippet_max_size)
45        } else {
46            org_mode.search(&query, limit, snippet_max_size)
47        };
48
49        match results {
50            Ok(results) => match Content::json(results) {
51                Ok(serialized) => Ok(CallToolResult::success(vec![serialized])),
52                Err(e) => Err(McpError {
53                    code: ErrorCode::INTERNAL_ERROR,
54                    message: format!("Failed to serialize search results: {e}").into(),
55                    data: None,
56                }),
57            },
58            Err(e) => {
59                let error_code = match &e {
60                    OrgModeError::InvalidDirectory(_) => ErrorCode::INVALID_PARAMS,
61                    OrgModeError::WalkError(_) => ErrorCode::INTERNAL_ERROR,
62                    OrgModeError::IoError(_) => ErrorCode::INTERNAL_ERROR,
63                    _ => ErrorCode::INTERNAL_ERROR,
64                };
65                Err(McpError {
66                    code: error_code,
67                    message: format!("Failed to search: {e}").into(),
68                    data: None,
69                })
70            }
71        }
72    }
73}