Skip to main content

codebones_mcp/
server.rs

1use std::path::Path;
2
3use rmcp::schemars::JsonSchema;
4use rmcp::serde::{Deserialize, Serialize};
5use rmcp::{
6    handler::server::{router::tool::ToolRouter, wrapper::Parameters},
7    tool, tool_handler, tool_router, ErrorData, Json, ServerHandler,
8};
9
10#[derive(Debug, Clone, Deserialize, JsonSchema)]
11#[serde(rename_all = "camelCase")]
12struct IndexArgs {
13    dir: String,
14}
15
16#[derive(Debug, Clone, Serialize, JsonSchema)]
17#[serde(rename_all = "camelCase")]
18struct IndexResponse {
19    dir: String,
20    status: String,
21}
22
23#[derive(Debug, Clone, Deserialize, JsonSchema)]
24#[serde(rename_all = "camelCase")]
25struct OutlineArgs {
26    dir: String,
27    path: String,
28}
29
30#[derive(Debug, Clone, Serialize, JsonSchema)]
31#[serde(rename_all = "camelCase")]
32struct OutlineResponse {
33    dir: String,
34    path: String,
35    outline: String,
36}
37
38#[derive(Debug, Clone, Deserialize, JsonSchema)]
39#[serde(rename_all = "camelCase")]
40struct GetArgs {
41    dir: String,
42    symbol_or_path: String,
43}
44
45#[derive(Debug, Clone, Serialize, JsonSchema)]
46#[serde(rename_all = "camelCase")]
47struct GetResponse {
48    dir: String,
49    symbol_or_path: String,
50    content: String,
51}
52
53#[derive(Debug, Clone, Deserialize, JsonSchema)]
54#[serde(rename_all = "camelCase")]
55struct SearchArgs {
56    dir: String,
57    query: String,
58}
59
60#[derive(Debug, Clone, Serialize, JsonSchema)]
61#[serde(rename_all = "camelCase")]
62struct SearchResponse {
63    dir: String,
64    query: String,
65    results: Vec<String>,
66}
67
68#[derive(Debug, Clone)]
69pub struct CodebonesMcpServer {
70    tool_router: ToolRouter<Self>,
71}
72
73impl Default for CodebonesMcpServer {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79#[tool_router(router = tool_router)]
80impl CodebonesMcpServer {
81    pub fn new() -> Self {
82        Self {
83            tool_router: Self::tool_router(),
84        }
85    }
86
87    fn invalid_params(tool: &str, message: String, data: rmcp::serde_json::Value) -> ErrorData {
88        ErrorData::invalid_params(format!("{tool} failed: {message}"), Some(data))
89    }
90
91    fn map_lookup_error(tool: &str, message: String, data: rmcp::serde_json::Value) -> ErrorData {
92        if message.contains("not found") || message.contains("No such file or directory") {
93            return ErrorData::resource_not_found(format!("{tool} failed: {message}"), Some(data));
94        }
95
96        if message.contains("invalid") {
97            return ErrorData::invalid_params(format!("{tool} failed: {message}"), Some(data));
98        }
99
100        ErrorData::internal_error(format!("{tool} failed: {message}"), Some(data))
101    }
102
103    fn ensure_dir(tool: &str, dir: &str) -> Result<(), ErrorData> {
104        let path = Path::new(dir);
105        if !path.exists() {
106            return Err(Self::invalid_params(
107                tool,
108                format!("directory does not exist: {dir}"),
109                rmcp::serde_json::json!({
110                    "tool": tool,
111                    "dir": dir,
112                }),
113            ));
114        }
115        if !path.is_dir() {
116            return Err(Self::invalid_params(
117                tool,
118                format!("path is not a directory: {dir}"),
119                rmcp::serde_json::json!({
120                    "tool": tool,
121                    "dir": dir,
122                }),
123            ));
124        }
125        Ok(())
126    }
127
128    #[tool(
129        name = "index",
130        description = "Builds or updates the codebones index for a directory"
131    )]
132    async fn index(
133        &self,
134        Parameters(IndexArgs { dir }): Parameters<IndexArgs>,
135    ) -> Result<Json<IndexResponse>, ErrorData> {
136        Self::ensure_dir("index", &dir)?;
137        let dir_path = Path::new(&dir);
138        codebones_core::api::index(dir_path).map_err(|error| {
139            ErrorData::internal_error(
140                format!("index failed: {}", error),
141                Some(rmcp::serde_json::json!({
142                    "tool": "index",
143                    "dir": dir,
144                })),
145            )
146        })?;
147
148        Ok(Json(IndexResponse {
149            dir,
150            status: "indexed".to_string(),
151        }))
152    }
153
154    #[tool(
155        name = "outline",
156        description = "Gets the skeleton outline of an indexed file"
157    )]
158    async fn outline(
159        &self,
160        Parameters(OutlineArgs { dir, path }): Parameters<OutlineArgs>,
161    ) -> Result<Json<OutlineResponse>, ErrorData> {
162        Self::ensure_dir("outline", &dir)?;
163        let outline = codebones_core::api::outline(Path::new(&dir), &path).map_err(|error| {
164            Self::map_lookup_error(
165                "outline",
166                error.to_string(),
167                rmcp::serde_json::json!({
168                    "tool": "outline",
169                    "dir": dir,
170                    "path": path,
171                }),
172            )
173        })?;
174
175        Ok(Json(OutlineResponse { dir, path, outline }))
176    }
177
178    #[tool(
179        name = "get",
180        description = "Retrieves the full source code for a specific symbol or file"
181    )]
182    async fn get(
183        &self,
184        Parameters(GetArgs {
185            dir,
186            symbol_or_path,
187        }): Parameters<GetArgs>,
188    ) -> Result<Json<GetResponse>, ErrorData> {
189        Self::ensure_dir("get", &dir)?;
190        let content =
191            codebones_core::api::get(Path::new(&dir), &symbol_or_path).map_err(|error| {
192                Self::map_lookup_error(
193                    "get",
194                    error.to_string(),
195                    rmcp::serde_json::json!({
196                        "tool": "get",
197                        "dir": dir,
198                        "symbol_or_path": symbol_or_path,
199                    }),
200                )
201            })?;
202
203        Ok(Json(GetResponse {
204            dir,
205            symbol_or_path,
206            content,
207        }))
208    }
209
210    #[tool(
211        name = "search",
212        description = "Searches for symbols across the repository"
213    )]
214    async fn search(
215        &self,
216        Parameters(SearchArgs { dir, query }): Parameters<SearchArgs>,
217    ) -> Result<Json<SearchResponse>, ErrorData> {
218        Self::ensure_dir("search", &dir)?;
219        let results = codebones_core::api::search(Path::new(&dir), &query).map_err(|error| {
220            Self::map_lookup_error(
221                "search",
222                error.to_string(),
223                rmcp::serde_json::json!({
224                    "tool": "search",
225                    "dir": dir,
226                    "query": query,
227                }),
228            )
229        })?;
230
231        Ok(Json(SearchResponse {
232            dir,
233            query,
234            results,
235        }))
236    }
237}
238
239#[tool_handler(router = self.tool_router)]
240impl ServerHandler for CodebonesMcpServer {}