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 {}