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