1use super::transport::{McpMessage, StdioTransport, Transport};
17use super::types::*;
18use anyhow::Result;
19use serde_json::{json, Value};
20use std::collections::HashMap;
21use std::sync::Arc;
22use tokio::sync::RwLock;
23use tracing::{debug, info, warn};
24
25pub struct McpServer {
27 transport: Arc<dyn Transport>,
28 tools: RwLock<HashMap<String, McpToolHandler>>,
29 resources: RwLock<HashMap<String, McpResourceHandler>>,
30 #[allow(dead_code)]
32 prompts: RwLock<HashMap<String, McpPromptHandler>>,
33 initialized: RwLock<bool>,
34 server_info: ServerInfo,
35 metadata: RwLock<HashMap<String, ToolMetadata>>,
37 resource_metadata: RwLock<HashMap<String, ResourceMetadata>>,
39}
40
41type McpToolHandler = Arc<dyn Fn(Value) -> Result<CallToolResult> + Send + Sync>;
42type McpResourceHandler = Arc<dyn Fn(String) -> Result<ReadResourceResult> + Send + Sync>;
43type McpPromptHandler = Arc<dyn Fn(Value) -> Result<GetPromptResult> + Send + Sync>;
44
45impl McpServer {
46 pub fn new_stdio() -> Self {
48 let transport = Arc::new(StdioTransport::new());
49 Self::new(transport)
50 }
51
52 pub fn new(transport: Arc<dyn Transport>) -> Self {
54 let mut server = Self {
55 transport,
56 tools: RwLock::new(HashMap::new()),
57 resources: RwLock::new(HashMap::new()),
58 prompts: RwLock::new(HashMap::new()),
59 initialized: RwLock::new(false),
60 server_info: ServerInfo {
61 name: "codetether".to_string(),
62 version: env!("CARGO_PKG_VERSION").to_string(),
63 },
64 metadata: RwLock::new(HashMap::new()),
65 resource_metadata: RwLock::new(HashMap::new()),
66 };
67
68 server.register_default_tools();
70
71 server
72 }
73
74 fn register_default_tools(&mut self) {
76 }
79
80 pub async fn register_tool(&self, name: &str, description: &str, input_schema: Value, handler: McpToolHandler) {
82 let metadata = ToolMetadata::new(
84 name.to_string(),
85 Some(description.to_string()),
86 input_schema.clone(),
87 );
88
89 let mut metadata_map = self.metadata.write().await;
90 metadata_map.insert(name.to_string(), metadata);
91 drop(metadata_map);
92
93 let mut tools = self.tools.write().await;
94 tools.insert(name.to_string(), handler);
95
96 debug!("Registered MCP tool: {}", name);
97 }
98
99 pub async fn register_resource(&self, uri: &str, name: &str, description: &str, mime_type: Option<&str>, handler: McpResourceHandler) {
101 let metadata = ResourceMetadata::new(
103 uri.to_string(),
104 name.to_string(),
105 Some(description.to_string()),
106 mime_type.map(|s| s.to_string()),
107 );
108
109 let mut metadata_map = self.resource_metadata.write().await;
110 metadata_map.insert(uri.to_string(), metadata);
111 drop(metadata_map);
112
113 let mut resources = self.resources.write().await;
114 resources.insert(uri.to_string(), handler);
115
116 debug!("Registered MCP resource: {}", uri);
117 }
118
119 pub async fn get_tool_metadata(&self, name: &str) -> Option<ToolMetadata> {
121 let metadata = self.metadata.read().await;
122 metadata.get(name).cloned()
123 }
124
125 pub async fn get_all_tool_metadata(&self) -> Vec<ToolMetadata> {
127 let metadata = self.metadata.read().await;
128 metadata.values().cloned().collect()
129 }
130
131 pub async fn get_resource_metadata(&self, uri: &str) -> Option<ResourceMetadata> {
133 let metadata = self.resource_metadata.read().await;
134 metadata.get(uri).cloned()
135 }
136
137 pub async fn get_all_resource_metadata(&self) -> Vec<ResourceMetadata> {
139 let metadata = self.resource_metadata.read().await;
140 metadata.values().cloned().collect()
141 }
142
143 pub async fn register_prompt(&self, name: &str, handler: McpPromptHandler) {
145 let mut prompts = self.prompts.write().await;
146 prompts.insert(name.to_string(), handler);
147 debug!("Registered MCP prompt: {}", name);
148 }
149
150 pub async fn get_prompt_handler(&self, name: &str) -> Option<McpPromptHandler> {
152 let prompts = self.prompts.read().await;
153 prompts.get(name).cloned()
154 }
155
156 pub async fn list_prompts(&self) -> Vec<String> {
158 let prompts = self.prompts.read().await;
159 prompts.keys().cloned().collect()
160 }
161
162 pub async fn run(&self) -> Result<()> {
164 info!("Starting MCP server...");
165
166 self.setup_tools().await;
168
169 loop {
170 match self.transport.receive().await? {
171 Some(McpMessage::Request(request)) => {
172 let response = self.handle_request(request).await;
173 self.transport.send_response(response).await?;
174 }
175 Some(McpMessage::Notification(notification)) => {
176 self.handle_notification(notification).await;
177 }
178 Some(McpMessage::Response(response)) => {
179 warn!("Unexpected response received: {:?}", response.id);
181 }
182 None => {
183 info!("Transport closed, shutting down MCP server");
184 break;
185 }
186 }
187 }
188
189 Ok(())
190 }
191
192 async fn setup_tools(&self) {
194 self.register_tool(
196 "run_command",
197 "Execute a shell command and return the output",
198 json!({
199 "type": "object",
200 "properties": {
201 "command": {
202 "type": "string",
203 "description": "The command to execute"
204 },
205 "cwd": {
206 "type": "string",
207 "description": "Working directory (optional)"
208 },
209 "timeout_ms": {
210 "type": "integer",
211 "description": "Timeout in milliseconds (default: 30000)"
212 }
213 },
214 "required": ["command"]
215 }),
216 Arc::new(|args| {
217 let command = args.get("command")
218 .and_then(|v| v.as_str())
219 .ok_or_else(|| anyhow::anyhow!("Missing command"))?;
220
221 let cwd = args.get("cwd").and_then(|v| v.as_str());
222
223 let mut cmd = std::process::Command::new("/bin/sh");
224 cmd.arg("-c").arg(command);
225
226 if let Some(dir) = cwd {
227 cmd.current_dir(dir);
228 }
229
230 let output = cmd.output()?;
231 let stdout = String::from_utf8_lossy(&output.stdout);
232 let stderr = String::from_utf8_lossy(&output.stderr);
233
234 let result = if output.status.success() {
235 format!("{}{}", stdout, stderr)
236 } else {
237 format!("Exit code: {}\n{}{}", output.status.code().unwrap_or(-1), stdout, stderr)
238 };
239
240 Ok(CallToolResult {
241 content: vec![ToolContent::Text { text: result }],
242 is_error: !output.status.success(),
243 })
244 }),
245 ).await;
246
247 self.register_tool(
249 "read_file",
250 "Read the contents of a file",
251 json!({
252 "type": "object",
253 "properties": {
254 "path": {
255 "type": "string",
256 "description": "Path to the file to read"
257 },
258 "offset": {
259 "type": "integer",
260 "description": "Line offset to start reading from (1-indexed)"
261 },
262 "limit": {
263 "type": "integer",
264 "description": "Maximum number of lines to read"
265 }
266 },
267 "required": ["path"]
268 }),
269 Arc::new(|args| {
270 let path = args.get("path")
271 .and_then(|v| v.as_str())
272 .ok_or_else(|| anyhow::anyhow!("Missing path"))?;
273
274 let content = std::fs::read_to_string(path)?;
275
276 let offset = args.get("offset").and_then(|v| v.as_u64()).unwrap_or(1) as usize;
277 let limit = args.get("limit").and_then(|v| v.as_u64());
278
279 let lines: Vec<&str> = content.lines().collect();
280 let start = (offset.saturating_sub(1)).min(lines.len());
281 let end = if let Some(l) = limit {
282 (start + l as usize).min(lines.len())
283 } else {
284 lines.len()
285 };
286
287 let result = lines[start..end].join("\n");
288
289 Ok(CallToolResult {
290 content: vec![ToolContent::Text { text: result }],
291 is_error: false,
292 })
293 }),
294 ).await;
295
296 self.register_tool(
298 "write_file",
299 "Write content to a file",
300 json!({
301 "type": "object",
302 "properties": {
303 "path": {
304 "type": "string",
305 "description": "Path to the file to write"
306 },
307 "content": {
308 "type": "string",
309 "description": "Content to write"
310 },
311 "create_dirs": {
312 "type": "boolean",
313 "description": "Create parent directories if they don't exist"
314 }
315 },
316 "required": ["path", "content"]
317 }),
318 Arc::new(|args| {
319 let path = args.get("path")
320 .and_then(|v| v.as_str())
321 .ok_or_else(|| anyhow::anyhow!("Missing path"))?;
322
323 let content = args.get("content")
324 .and_then(|v| v.as_str())
325 .ok_or_else(|| anyhow::anyhow!("Missing content"))?;
326
327 let create_dirs = args.get("create_dirs")
328 .and_then(|v| v.as_bool())
329 .unwrap_or(false);
330
331 if create_dirs {
332 if let Some(parent) = std::path::Path::new(path).parent() {
333 std::fs::create_dir_all(parent)?;
334 }
335 }
336
337 std::fs::write(path, content)?;
338
339 Ok(CallToolResult {
340 content: vec![ToolContent::Text { text: format!("Wrote {} bytes to {}", content.len(), path) }],
341 is_error: false,
342 })
343 }),
344 ).await;
345
346 self.register_tool(
348 "list_directory",
349 "List contents of a directory",
350 json!({
351 "type": "object",
352 "properties": {
353 "path": {
354 "type": "string",
355 "description": "Path to the directory"
356 },
357 "recursive": {
358 "type": "boolean",
359 "description": "List recursively"
360 },
361 "max_depth": {
362 "type": "integer",
363 "description": "Maximum depth for recursive listing"
364 }
365 },
366 "required": ["path"]
367 }),
368 Arc::new(|args| {
369 let path = args.get("path")
370 .and_then(|v| v.as_str())
371 .ok_or_else(|| anyhow::anyhow!("Missing path"))?;
372
373 let mut entries = Vec::new();
374 for entry in std::fs::read_dir(path)? {
375 let entry = entry?;
376 let file_type = entry.file_type()?;
377 let name = entry.file_name().to_string_lossy().to_string();
378 let suffix = if file_type.is_dir() { "/" } else { "" };
379 entries.push(format!("{}{}", name, suffix));
380 }
381
382 entries.sort();
383
384 Ok(CallToolResult {
385 content: vec![ToolContent::Text { text: entries.join("\n") }],
386 is_error: false,
387 })
388 }),
389 ).await;
390
391 self.register_tool(
393 "search_files",
394 "Search for files matching a pattern",
395 json!({
396 "type": "object",
397 "properties": {
398 "pattern": {
399 "type": "string",
400 "description": "Search pattern (glob or regex)"
401 },
402 "path": {
403 "type": "string",
404 "description": "Directory to search in"
405 },
406 "content_pattern": {
407 "type": "string",
408 "description": "Pattern to search in file contents"
409 }
410 },
411 "required": ["pattern"]
412 }),
413 Arc::new(|args| {
414 let pattern = args.get("pattern")
415 .and_then(|v| v.as_str())
416 .ok_or_else(|| anyhow::anyhow!("Missing pattern"))?;
417
418 let path = args.get("path")
419 .and_then(|v| v.as_str())
420 .unwrap_or(".");
421
422 let output = std::process::Command::new("find")
424 .args([path, "-name", pattern, "-type", "f"])
425 .output()?;
426
427 let result = String::from_utf8_lossy(&output.stdout);
428
429 Ok(CallToolResult {
430 content: vec![ToolContent::Text { text: result.to_string() }],
431 is_error: !output.status.success(),
432 })
433 }),
434 ).await;
435
436 self.register_tool(
438 "grep_search",
439 "Search file contents using grep",
440 json!({
441 "type": "object",
442 "properties": {
443 "query": {
444 "type": "string",
445 "description": "Search pattern"
446 },
447 "path": {
448 "type": "string",
449 "description": "Directory or file to search"
450 },
451 "is_regex": {
452 "type": "boolean",
453 "description": "Treat pattern as regex"
454 },
455 "case_sensitive": {
456 "type": "boolean",
457 "description": "Case-sensitive search"
458 }
459 },
460 "required": ["query"]
461 }),
462 Arc::new(|args| {
463 let query = args.get("query")
464 .and_then(|v| v.as_str())
465 .ok_or_else(|| anyhow::anyhow!("Missing query"))?;
466
467 let path = args.get("path")
468 .and_then(|v| v.as_str())
469 .unwrap_or(".");
470
471 let is_regex = args.get("is_regex")
472 .and_then(|v| v.as_bool())
473 .unwrap_or(false);
474
475 let case_sensitive = args.get("case_sensitive")
476 .and_then(|v| v.as_bool())
477 .unwrap_or(false);
478
479 let mut cmd = std::process::Command::new("grep");
480 cmd.arg("-r").arg("-n");
481
482 if !case_sensitive {
483 cmd.arg("-i");
484 }
485
486 if is_regex {
487 cmd.arg("-E");
488 } else {
489 cmd.arg("-F");
490 }
491
492 cmd.arg(query).arg(path);
493
494 let output = cmd.output()?;
495 let result = String::from_utf8_lossy(&output.stdout);
496
497 Ok(CallToolResult {
498 content: vec![ToolContent::Text { text: result.to_string() }],
499 is_error: false,
500 })
501 }),
502 ).await;
503
504 info!("Registered {} MCP tools", self.tools.read().await.len());
505 }
506
507 async fn handle_request(&self, request: JsonRpcRequest) -> JsonRpcResponse {
509 debug!("Handling request: {} (id: {:?})", request.method, request.id);
510
511 let result = match request.method.as_str() {
512 "initialize" => self.handle_initialize(request.params).await,
513 "initialized" => Ok(json!({})),
514 "ping" => Ok(json!({})),
515 "tools/list" => self.handle_list_tools(request.params).await,
516 "tools/call" => self.handle_call_tool(request.params).await,
517 "resources/list" => self.handle_list_resources(request.params).await,
518 "resources/read" => self.handle_read_resource(request.params).await,
519 "prompts/list" => self.handle_list_prompts(request.params).await,
520 "prompts/get" => self.handle_get_prompt(request.params).await,
521 _ => Err(JsonRpcError::method_not_found(&request.method)),
522 };
523
524 match result {
525 Ok(value) => JsonRpcResponse::success(request.id, value),
526 Err(error) => JsonRpcResponse::error(request.id, error),
527 }
528 }
529
530 async fn handle_notification(&self, notification: JsonRpcNotification) {
532 debug!("Handling notification: {}", notification.method);
533
534 match notification.method.as_str() {
535 "notifications/initialized" => {
536 *self.initialized.write().await = true;
537 info!("MCP client initialized");
538 }
539 "notifications/cancelled" => {
540 }
542 _ => {
543 debug!("Unknown notification: {}", notification.method);
544 }
545 }
546 }
547
548 async fn handle_initialize(&self, params: Option<Value>) -> Result<Value, JsonRpcError> {
550 let _params: InitializeParams = if let Some(p) = params {
551 serde_json::from_value(p).map_err(|e| JsonRpcError::invalid_params(e.to_string()))?
552 } else {
553 return Err(JsonRpcError::invalid_params("Missing params"));
554 };
555
556 let result = InitializeResult {
557 protocol_version: PROTOCOL_VERSION.to_string(),
558 capabilities: ServerCapabilities {
559 tools: Some(ToolsCapability { list_changed: true }),
560 resources: Some(ResourcesCapability { subscribe: false, list_changed: true }),
561 prompts: Some(PromptsCapability { list_changed: true }),
562 logging: Some(LoggingCapability {}),
563 experimental: None,
564 },
565 server_info: self.server_info.clone(),
566 instructions: Some(
567 "CodeTether is an AI coding agent with tools for file operations, \
568 command execution, code search, and autonomous task execution. \
569 Use the swarm tool for complex tasks requiring parallel execution, \
570 and ralph for PRD-driven development.".to_string()
571 ),
572 };
573
574 serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string()))
575 }
576
577 async fn handle_list_tools(&self, _params: Option<Value>) -> Result<Value, JsonRpcError> {
579 let _tools = self.tools.read().await;
580
581 let tool_list: Vec<McpTool> = vec![
582 McpTool {
583 name: "run_command".to_string(),
584 description: Some("Execute a shell command".to_string()),
585 input_schema: json!({
586 "type": "object",
587 "properties": {
588 "command": { "type": "string" },
589 "cwd": { "type": "string" }
590 },
591 "required": ["command"]
592 }),
593 },
594 McpTool {
595 name: "read_file".to_string(),
596 description: Some("Read file contents".to_string()),
597 input_schema: json!({
598 "type": "object",
599 "properties": {
600 "path": { "type": "string" },
601 "offset": { "type": "integer" },
602 "limit": { "type": "integer" }
603 },
604 "required": ["path"]
605 }),
606 },
607 McpTool {
608 name: "write_file".to_string(),
609 description: Some("Write content to a file".to_string()),
610 input_schema: json!({
611 "type": "object",
612 "properties": {
613 "path": { "type": "string" },
614 "content": { "type": "string" }
615 },
616 "required": ["path", "content"]
617 }),
618 },
619 McpTool {
620 name: "list_directory".to_string(),
621 description: Some("List directory contents".to_string()),
622 input_schema: json!({
623 "type": "object",
624 "properties": {
625 "path": { "type": "string" }
626 },
627 "required": ["path"]
628 }),
629 },
630 McpTool {
631 name: "search_files".to_string(),
632 description: Some("Search for files by name pattern".to_string()),
633 input_schema: json!({
634 "type": "object",
635 "properties": {
636 "pattern": { "type": "string" },
637 "path": { "type": "string" }
638 },
639 "required": ["pattern"]
640 }),
641 },
642 McpTool {
643 name: "grep_search".to_string(),
644 description: Some("Search file contents".to_string()),
645 input_schema: json!({
646 "type": "object",
647 "properties": {
648 "query": { "type": "string" },
649 "path": { "type": "string" },
650 "is_regex": { "type": "boolean" }
651 },
652 "required": ["query"]
653 }),
654 },
655 ];
656
657 let result = ListToolsResult {
658 tools: tool_list,
659 next_cursor: None,
660 };
661
662 serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string()))
663 }
664
665 async fn handle_call_tool(&self, params: Option<Value>) -> Result<Value, JsonRpcError> {
667 let params: CallToolParams = if let Some(p) = params {
668 serde_json::from_value(p).map_err(|e| JsonRpcError::invalid_params(e.to_string()))?
669 } else {
670 return Err(JsonRpcError::invalid_params("Missing params"));
671 };
672
673 let tools = self.tools.read().await;
674 let handler = tools.get(¶ms.name)
675 .ok_or_else(|| JsonRpcError::method_not_found(¶ms.name))?;
676
677 match handler(params.arguments) {
678 Ok(result) => serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string())),
679 Err(e) => {
680 let result = CallToolResult {
681 content: vec![ToolContent::Text { text: e.to_string() }],
682 is_error: true,
683 };
684 serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string()))
685 }
686 }
687 }
688
689 async fn handle_list_resources(&self, _params: Option<Value>) -> Result<Value, JsonRpcError> {
691 let result = ListResourcesResult {
692 resources: vec![],
693 next_cursor: None,
694 };
695
696 serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string()))
697 }
698
699 async fn handle_read_resource(&self, params: Option<Value>) -> Result<Value, JsonRpcError> {
701 let params: ReadResourceParams = if let Some(p) = params {
702 serde_json::from_value(p).map_err(|e| JsonRpcError::invalid_params(e.to_string()))?
703 } else {
704 return Err(JsonRpcError::invalid_params("Missing params"));
705 };
706
707 let resources = self.resources.read().await;
708 let handler = resources.get(¶ms.uri)
709 .ok_or_else(|| JsonRpcError::method_not_found(¶ms.uri))?;
710
711 match handler(params.uri.clone()) {
712 Ok(result) => serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string())),
713 Err(e) => Err(JsonRpcError::internal_error(e.to_string())),
714 }
715 }
716
717 async fn handle_list_prompts(&self, _params: Option<Value>) -> Result<Value, JsonRpcError> {
719 let result = ListPromptsResult {
720 prompts: vec![
721 McpPrompt {
722 name: "code_review".to_string(),
723 description: Some("Review code for issues and improvements".to_string()),
724 arguments: vec![
725 PromptArgument {
726 name: "file".to_string(),
727 description: Some("File to review".to_string()),
728 required: true,
729 },
730 ],
731 },
732 McpPrompt {
733 name: "explain_code".to_string(),
734 description: Some("Explain what code does".to_string()),
735 arguments: vec![
736 PromptArgument {
737 name: "file".to_string(),
738 description: Some("File to explain".to_string()),
739 required: true,
740 },
741 ],
742 },
743 ],
744 next_cursor: None,
745 };
746
747 serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string()))
748 }
749
750 async fn handle_get_prompt(&self, params: Option<Value>) -> Result<Value, JsonRpcError> {
752 let params: GetPromptParams = if let Some(p) = params {
753 serde_json::from_value(p).map_err(|e| JsonRpcError::invalid_params(e.to_string()))?
754 } else {
755 return Err(JsonRpcError::invalid_params("Missing params"));
756 };
757
758 let result = match params.name.as_str() {
759 "code_review" => {
760 let file = params.arguments.get("file")
761 .and_then(|v| v.as_str())
762 .unwrap_or("file.rs");
763
764 GetPromptResult {
765 description: Some("Code review prompt".to_string()),
766 messages: vec![
767 PromptMessage {
768 role: PromptRole::User,
769 content: PromptContent::Text {
770 text: format!(
771 "Please review the following code for:\n\
772 - Bugs and potential issues\n\
773 - Performance concerns\n\
774 - Code style and best practices\n\
775 - Security vulnerabilities\n\n\
776 File: {}", file
777 ),
778 },
779 },
780 ],
781 }
782 }
783 "explain_code" => {
784 let file = params.arguments.get("file")
785 .and_then(|v| v.as_str())
786 .unwrap_or("file.rs");
787
788 GetPromptResult {
789 description: Some("Code explanation prompt".to_string()),
790 messages: vec![
791 PromptMessage {
792 role: PromptRole::User,
793 content: PromptContent::Text {
794 text: format!(
795 "Please explain what this code does, including:\n\
796 - Overall purpose\n\
797 - Key functions and their roles\n\
798 - Data flow\n\
799 - Important algorithms used\n\n\
800 File: {}", file
801 ),
802 },
803 },
804 ],
805 }
806 }
807 _ => return Err(JsonRpcError::method_not_found(¶ms.name)),
808 };
809
810 serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(e.to_string()))
811 }
812}