codeprism_mcp/
lib.rs

1//! # CodePrism MCP Server
2//!
3//! A Model Context Protocol (MCP) compliant server that provides access to code repositories
4//! through standardized Resources, Tools, and Prompts.
5//!
6//! This implementation follows the MCP specification for JSON-RPC 2.0 communication
7//! over stdio transport, enabling integration with MCP clients like Claude Desktop,
8//! Cursor, and other AI applications.
9
10use anyhow::Result;
11
12use codeprism_core::{
13    ast::{Edge, Language, Node},
14    graph::{GraphQuery, GraphStore},
15    indexer::BulkIndexer,
16    parser::{LanguageParser, ParseContext, ParseResult, ParserEngine},
17    repository::RepositoryManager,
18    scanner::RepositoryScanner,
19};
20use std::collections::HashMap;
21use std::path::Path;
22use std::sync::Arc;
23
24pub mod context;
25pub mod prompts;
26pub mod protocol;
27pub mod resources;
28pub mod server;
29pub mod tools;
30pub mod tools_legacy;
31pub mod transport;
32
33// Re-export main types
34pub use protocol::{
35    InitializeParams, InitializeResult, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse,
36    ServerCapabilities,
37};
38pub use server::McpServer;
39pub use transport::{StdioTransport, Transport};
40
41/// Python language parser adapter
42struct PythonParserAdapter;
43
44impl LanguageParser for PythonParserAdapter {
45    fn language(&self) -> Language {
46        Language::Python
47    }
48
49    fn parse(&self, context: &ParseContext) -> codeprism_core::error::Result<ParseResult> {
50        // Use the Python parser from codeprism-lang-python
51        let python_parser = codeprism_lang_python::PythonLanguageParser::new();
52
53        match codeprism_lang_python::parse_file(
54            &python_parser,
55            &context.repo_id,
56            context.file_path.clone(),
57            context.content.clone(),
58            context.old_tree.clone(),
59        ) {
60            Ok((tree, py_nodes, py_edges)) => {
61                // Convert Python parser types to codeprism types
62                let nodes: Vec<Node> = py_nodes
63                    .into_iter()
64                    .map(|py_node| {
65                        // Convert NodeKind
66                        let codeprism_kind = match py_node.kind {
67                            codeprism_lang_python::NodeKind::Function => {
68                                codeprism_core::ast::NodeKind::Function
69                            }
70                            codeprism_lang_python::NodeKind::Class => {
71                                codeprism_core::ast::NodeKind::Class
72                            }
73                            codeprism_lang_python::NodeKind::Variable => {
74                                codeprism_core::ast::NodeKind::Variable
75                            }
76                            codeprism_lang_python::NodeKind::Module => {
77                                codeprism_core::ast::NodeKind::Module
78                            }
79                            codeprism_lang_python::NodeKind::Import => {
80                                codeprism_core::ast::NodeKind::Import
81                            }
82                            codeprism_lang_python::NodeKind::Parameter => {
83                                codeprism_core::ast::NodeKind::Parameter
84                            }
85                            codeprism_lang_python::NodeKind::Method => {
86                                codeprism_core::ast::NodeKind::Method
87                            }
88                            codeprism_lang_python::NodeKind::Call => {
89                                codeprism_core::ast::NodeKind::Call
90                            }
91                            codeprism_lang_python::NodeKind::Literal => {
92                                codeprism_core::ast::NodeKind::Literal
93                            }
94                            codeprism_lang_python::NodeKind::Route => {
95                                codeprism_core::ast::NodeKind::Route
96                            }
97                            codeprism_lang_python::NodeKind::SqlQuery => {
98                                codeprism_core::ast::NodeKind::SqlQuery
99                            }
100                            codeprism_lang_python::NodeKind::Event => {
101                                codeprism_core::ast::NodeKind::Event
102                            }
103                            codeprism_lang_python::NodeKind::Unknown => {
104                                codeprism_core::ast::NodeKind::Unknown
105                            }
106                        };
107
108                        // Convert Span
109                        let codeprism_span = codeprism_core::ast::Span::new(
110                            py_node.span.start_byte,
111                            py_node.span.end_byte,
112                            py_node.span.start_line,
113                            py_node.span.end_line,
114                            py_node.span.start_column,
115                            py_node.span.end_column,
116                        );
117
118                        Node::new(
119                            &context.repo_id,
120                            codeprism_kind,
121                            py_node.name,
122                            Language::Python,
123                            context.file_path.clone(),
124                            codeprism_span,
125                        )
126                    })
127                    .collect();
128
129                let edges: Vec<Edge> = py_edges
130                    .into_iter()
131                    .map(|py_edge| {
132                        // Convert EdgeKind
133                        let codeprism_edge_kind = match py_edge.kind {
134                            codeprism_lang_python::EdgeKind::Calls => {
135                                codeprism_core::ast::EdgeKind::Calls
136                            }
137                            codeprism_lang_python::EdgeKind::Reads => {
138                                codeprism_core::ast::EdgeKind::Reads
139                            }
140                            codeprism_lang_python::EdgeKind::Writes => {
141                                codeprism_core::ast::EdgeKind::Writes
142                            }
143                            codeprism_lang_python::EdgeKind::Imports => {
144                                codeprism_core::ast::EdgeKind::Imports
145                            }
146                            codeprism_lang_python::EdgeKind::Emits => {
147                                codeprism_core::ast::EdgeKind::Emits
148                            }
149                            codeprism_lang_python::EdgeKind::RoutesTo => {
150                                codeprism_core::ast::EdgeKind::RoutesTo
151                            }
152                            codeprism_lang_python::EdgeKind::Raises => {
153                                codeprism_core::ast::EdgeKind::Raises
154                            }
155                            codeprism_lang_python::EdgeKind::Extends => {
156                                codeprism_core::ast::EdgeKind::Extends
157                            }
158                            codeprism_lang_python::EdgeKind::Implements => {
159                                codeprism_core::ast::EdgeKind::Implements
160                            }
161                        };
162
163                        // Convert NodeIds by using hex representation
164                        let codecodeprism_source =
165                            codeprism_core::ast::NodeId::from_hex(&py_edge.source.to_hex())
166                                .unwrap();
167                        let codeprism_target =
168                            codeprism_core::ast::NodeId::from_hex(&py_edge.target.to_hex())
169                                .unwrap();
170
171                        Edge::new(codecodeprism_source, codeprism_target, codeprism_edge_kind)
172                    })
173                    .collect();
174
175                Ok(ParseResult { tree, nodes, edges })
176            }
177            Err(e) => Err(codeprism_core::error::Error::parse(
178                &context.file_path,
179                format!("Python parsing failed: {}", e),
180            )),
181        }
182    }
183}
184
185/// JavaScript language parser adapter
186struct JavaScriptParserAdapter;
187
188impl LanguageParser for JavaScriptParserAdapter {
189    fn language(&self) -> Language {
190        Language::JavaScript
191    }
192
193    fn parse(&self, context: &ParseContext) -> codeprism_core::error::Result<ParseResult> {
194        // Use the JavaScript parser from codeprism-lang-js
195        let js_parser = codeprism_lang_js::JavaScriptLanguageParser::new();
196
197        match codeprism_lang_js::parse_file(
198            &js_parser,
199            &context.repo_id,
200            context.file_path.clone(),
201            context.content.clone(),
202            context.old_tree.clone(),
203        ) {
204            Ok((tree, js_nodes, js_edges)) => {
205                // Convert JavaScript parser types to codeprism types
206                let nodes: Vec<Node> = js_nodes
207                    .into_iter()
208                    .map(|js_node| {
209                        // Convert NodeKind
210                        let codeprism_kind = match js_node.kind {
211                            codeprism_lang_js::NodeKind::Function => {
212                                codeprism_core::ast::NodeKind::Function
213                            }
214                            codeprism_lang_js::NodeKind::Class => {
215                                codeprism_core::ast::NodeKind::Class
216                            }
217                            codeprism_lang_js::NodeKind::Variable => {
218                                codeprism_core::ast::NodeKind::Variable
219                            }
220                            codeprism_lang_js::NodeKind::Module => {
221                                codeprism_core::ast::NodeKind::Module
222                            }
223                            codeprism_lang_js::NodeKind::Import => {
224                                codeprism_core::ast::NodeKind::Import
225                            }
226                            codeprism_lang_js::NodeKind::Parameter => {
227                                codeprism_core::ast::NodeKind::Parameter
228                            }
229                            codeprism_lang_js::NodeKind::Method => {
230                                codeprism_core::ast::NodeKind::Method
231                            }
232                            codeprism_lang_js::NodeKind::Call => {
233                                codeprism_core::ast::NodeKind::Call
234                            }
235                            codeprism_lang_js::NodeKind::Literal => {
236                                codeprism_core::ast::NodeKind::Literal
237                            }
238                            codeprism_lang_js::NodeKind::Route => {
239                                codeprism_core::ast::NodeKind::Route
240                            }
241                            codeprism_lang_js::NodeKind::SqlQuery => {
242                                codeprism_core::ast::NodeKind::SqlQuery
243                            }
244                            codeprism_lang_js::NodeKind::Event => {
245                                codeprism_core::ast::NodeKind::Event
246                            }
247                            codeprism_lang_js::NodeKind::Unknown => {
248                                codeprism_core::ast::NodeKind::Unknown
249                            }
250                        };
251
252                        // Convert Span
253                        let codeprism_span = codeprism_core::ast::Span::new(
254                            js_node.span.start_byte,
255                            js_node.span.end_byte,
256                            js_node.span.start_line,
257                            js_node.span.end_line,
258                            js_node.span.start_column,
259                            js_node.span.end_column,
260                        );
261
262                        Node::new(
263                            &context.repo_id,
264                            codeprism_kind,
265                            js_node.name,
266                            Language::JavaScript,
267                            context.file_path.clone(),
268                            codeprism_span,
269                        )
270                    })
271                    .collect();
272
273                let edges: Vec<Edge> = js_edges
274                    .into_iter()
275                    .map(|js_edge| {
276                        // Convert EdgeKind
277                        let codeprism_edge_kind = match js_edge.kind {
278                            codeprism_lang_js::EdgeKind::Calls => {
279                                codeprism_core::ast::EdgeKind::Calls
280                            }
281                            codeprism_lang_js::EdgeKind::Reads => {
282                                codeprism_core::ast::EdgeKind::Reads
283                            }
284                            codeprism_lang_js::EdgeKind::Writes => {
285                                codeprism_core::ast::EdgeKind::Writes
286                            }
287                            codeprism_lang_js::EdgeKind::Imports => {
288                                codeprism_core::ast::EdgeKind::Imports
289                            }
290                            codeprism_lang_js::EdgeKind::Emits => {
291                                codeprism_core::ast::EdgeKind::Emits
292                            }
293                            codeprism_lang_js::EdgeKind::RoutesTo => {
294                                codeprism_core::ast::EdgeKind::RoutesTo
295                            }
296                            codeprism_lang_js::EdgeKind::Raises => {
297                                codeprism_core::ast::EdgeKind::Raises
298                            }
299                            codeprism_lang_js::EdgeKind::Extends => {
300                                codeprism_core::ast::EdgeKind::Extends
301                            }
302                            codeprism_lang_js::EdgeKind::Implements => {
303                                codeprism_core::ast::EdgeKind::Implements
304                            }
305                        };
306
307                        // Convert NodeIds by using hex representation
308                        let codecodeprism_source =
309                            codeprism_core::ast::NodeId::from_hex(&js_edge.source.to_hex())
310                                .unwrap();
311                        let codeprism_target =
312                            codeprism_core::ast::NodeId::from_hex(&js_edge.target.to_hex())
313                                .unwrap();
314
315                        Edge::new(codecodeprism_source, codeprism_target, codeprism_edge_kind)
316                    })
317                    .collect();
318
319                Ok(ParseResult { tree, nodes, edges })
320            }
321            Err(e) => Err(codeprism_core::error::Error::parse(
322                &context.file_path,
323                format!("JavaScript parsing failed: {}", e),
324            )),
325        }
326    }
327}
328
329/// MCP Server implementation that integrates with CodePrism Phase 2.5 components
330pub struct CodePrismMcpServer {
331    /// Repository manager from Phase 2.5
332    repository_manager: RepositoryManager,
333    /// Repository scanner for file discovery
334    scanner: RepositoryScanner,
335    /// Bulk indexer for processing files
336    indexer: BulkIndexer,
337    /// Parser engine for language processing
338    parser_engine: std::sync::Arc<ParserEngine>,
339    /// Graph store for code intelligence
340    graph_store: Arc<GraphStore>,
341    /// Graph query engine
342    graph_query: GraphQuery,
343    /// Content search manager for full-text search
344    content_search: Arc<codeprism_core::ContentSearchManager>,
345    /// Server capabilities
346    capabilities: ServerCapabilities,
347    /// Current repository path
348    repository_path: Option<std::path::PathBuf>,
349}
350
351impl CodePrismMcpServer {
352    /// Create a new MCP server instance
353    pub fn new() -> Result<Self> {
354        let language_registry =
355            std::sync::Arc::new(codeprism_core::parser::LanguageRegistry::new());
356
357        // Register language parsers
358        language_registry.register(Arc::new(PythonParserAdapter));
359        language_registry.register(Arc::new(JavaScriptParserAdapter));
360
361        let parser_engine = std::sync::Arc::new(ParserEngine::new(language_registry.clone()));
362        let repository_manager = RepositoryManager::new(language_registry);
363        let scanner = RepositoryScanner::new();
364        let indexer = BulkIndexer::new(
365            codeprism_core::indexer::IndexingConfig::new("mcp".to_string(), "default".to_string()),
366            parser_engine.clone(),
367        );
368
369        let graph_store = Arc::new(GraphStore::new());
370        let graph_query = GraphQuery::new(graph_store.clone());
371        let content_search = Arc::new(codeprism_core::ContentSearchManager::with_graph_store(
372            graph_store.clone(),
373        ));
374
375        let capabilities = ServerCapabilities {
376            resources: Some(resources::ResourceCapabilities {
377                subscribe: Some(true),
378                list_changed: Some(true),
379            }),
380            tools: Some(tools::ToolCapabilities {
381                list_changed: Some(true),
382            }),
383            prompts: Some(prompts::PromptCapabilities {
384                list_changed: Some(false),
385            }),
386            experimental: Some(HashMap::new()),
387        };
388
389        Ok(Self {
390            repository_manager,
391            scanner,
392            indexer,
393            parser_engine,
394            graph_store,
395            graph_query,
396            content_search,
397            capabilities,
398            repository_path: None,
399        })
400    }
401
402    /// Create a new MCP server instance with custom configuration
403    pub fn new_with_config(
404        memory_limit_mb: usize,
405        batch_size: usize,
406        max_file_size_mb: usize,
407        disable_memory_limit: bool,
408        exclude_dirs: Vec<String>,
409        include_extensions: Option<Vec<String>>,
410        dependency_mode: Option<String>,
411    ) -> Result<Self> {
412        let language_registry =
413            std::sync::Arc::new(codeprism_core::parser::LanguageRegistry::new());
414
415        // Register language parsers
416        language_registry.register(Arc::new(PythonParserAdapter));
417        language_registry.register(Arc::new(JavaScriptParserAdapter));
418
419        let parser_engine = std::sync::Arc::new(ParserEngine::new(language_registry.clone()));
420
421        // Parse dependency mode
422        let dep_mode = match dependency_mode.as_deref() {
423            Some("include_all") => codeprism_core::scanner::DependencyMode::IncludeAll,
424            Some("smart") => codeprism_core::scanner::DependencyMode::Smart,
425            _ => codeprism_core::scanner::DependencyMode::Exclude,
426        };
427
428        // Create repository manager with custom configuration
429        let repository_manager = RepositoryManager::new_with_config(
430            language_registry,
431            Some(exclude_dirs.clone()),
432            include_extensions.clone(),
433            Some(dep_mode.clone()),
434        );
435
436        // Create scanner with custom configuration for direct use
437        let scanner = if !exclude_dirs.is_empty() {
438            let mut scanner = RepositoryScanner::with_exclude_dirs(exclude_dirs.clone())
439                .with_dependency_mode(dep_mode.clone());
440            if let Some(ref extensions) = include_extensions {
441                scanner = scanner.with_extensions(extensions.clone());
442            }
443            scanner
444        } else {
445            RepositoryScanner::new().with_dependency_mode(dep_mode.clone())
446        };
447
448        // Create custom indexing config with user settings
449        let mut indexing_config =
450            codeprism_core::indexer::IndexingConfig::new("mcp".to_string(), "default".to_string());
451        indexing_config.batch_size = batch_size;
452
453        if disable_memory_limit {
454            indexing_config.memory_limit = None;
455            tracing::warn!(
456                "Memory limit checking disabled - use with caution for large repositories"
457            );
458        } else {
459            indexing_config.memory_limit = Some(memory_limit_mb * 1024 * 1024); // Convert MB to bytes
460        }
461
462        let indexer = BulkIndexer::new(indexing_config, parser_engine.clone());
463
464        let graph_store = Arc::new(GraphStore::new());
465        let graph_query = GraphQuery::new(graph_store.clone());
466        let content_search = Arc::new(codeprism_core::ContentSearchManager::with_graph_store(
467            graph_store.clone(),
468        ));
469
470        let capabilities = ServerCapabilities {
471            resources: Some(resources::ResourceCapabilities {
472                subscribe: Some(true),
473                list_changed: Some(true),
474            }),
475            tools: Some(tools::ToolCapabilities {
476                list_changed: Some(true),
477            }),
478            prompts: Some(prompts::PromptCapabilities {
479                list_changed: Some(false),
480            }),
481            experimental: Some(HashMap::new()),
482        };
483
484        tracing::info!("MCP server configured with:");
485        tracing::info!(
486            "  Memory limit: {}MB{}",
487            memory_limit_mb,
488            if disable_memory_limit {
489                " (disabled)"
490            } else {
491                ""
492            }
493        );
494        tracing::info!("  Batch size: {}", batch_size);
495        tracing::info!("  Max file size: {}MB", max_file_size_mb);
496        tracing::info!("  Dependency mode: {:?}", dep_mode);
497        tracing::info!("  Exclude directories: {:?}", exclude_dirs);
498        if let Some(ref exts) = include_extensions {
499            tracing::info!("  Include extensions: {:?}", exts);
500        }
501
502        Ok(Self {
503            repository_manager,
504            scanner,
505            indexer,
506            parser_engine,
507            graph_store,
508            graph_query,
509            content_search,
510            capabilities,
511            repository_path: None,
512        })
513    }
514
515    /// Initialize the server with a repository path
516    pub async fn initialize_with_repository<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
517        let path = path.as_ref().to_path_buf();
518
519        // Create repository config
520        let repo_id = format!(
521            "mcp-{}",
522            path.file_name()
523                .and_then(|n| n.to_str())
524                .unwrap_or("repository")
525        );
526
527        let repo_config = codeprism_core::repository::RepositoryConfig::new(repo_id.clone(), &path)
528            .with_name(
529                path.file_name()
530                    .and_then(|n| n.to_str())
531                    .unwrap_or("repository")
532                    .to_string(),
533            );
534
535        // Register repository
536        self.repository_manager.register_repository(repo_config)?;
537
538        // Perform initial scan and indexing for code symbols
539        let indexing_result = self
540            .repository_manager
541            .index_repository(&repo_id, None)
542            .await?;
543
544        // Populate graph store with indexed data
545        for patch in &indexing_result.patches {
546            for node in &patch.nodes_add {
547                self.graph_store.add_node(node.clone());
548            }
549            for edge in &patch.edges_add {
550                self.graph_store.add_edge(edge.clone());
551            }
552        }
553
554        // Index content for documentation, configuration files, and comments
555        self.index_repository_content(&path).await?;
556
557        self.repository_path = Some(path);
558        tracing::info!(
559            "MCP server initialized with repository: {:?}",
560            self.repository_path
561        );
562
563        Ok(())
564    }
565
566    /// Index repository content including documentation, configuration, and comments
567    async fn index_repository_content(&self, repo_path: &Path) -> Result<()> {
568        tracing::info!("Starting content indexing for repository: {:?}", repo_path);
569
570        // Discover all files in the repository
571        let files = self.scanner.discover_files(repo_path)?;
572        let mut indexed_count = 0;
573        let mut error_count = 0;
574
575        for file_path in files {
576            if let Err(e) = self.index_file_content(&file_path).await {
577                tracing::warn!("Failed to index content for {}: {}", file_path.display(), e);
578                error_count += 1;
579            } else {
580                indexed_count += 1;
581            }
582        }
583
584        tracing::info!(
585            "Content indexing completed: {} files indexed, {} errors",
586            indexed_count,
587            error_count
588        );
589        Ok(())
590    }
591
592    /// Index content for a single file
593    async fn index_file_content(&self, file_path: &Path) -> Result<()> {
594        // Read file content
595        let content = match std::fs::read_to_string(file_path) {
596            Ok(content) => content,
597            Err(_) => {
598                // Skip binary files or files that can't be read as text
599                return Ok(());
600            }
601        };
602
603        // Skip empty files
604        if content.trim().is_empty() {
605            return Ok(());
606        }
607
608        let _language = self.detect_language(file_path);
609
610        // Handle different file types appropriately
611        // For now, use simple file indexing for all content types
612        // TODO: In the future, we can enhance this with tree-sitter integration
613        // to extract comments and provide better source code content indexing
614        self.content_search.index_file(file_path, &content)?;
615
616        Ok(())
617    }
618
619    /// Detect programming language from file extension
620    fn detect_language(&self, file_path: &Path) -> Option<codeprism_core::ast::Language> {
621        let extension = file_path.extension()?.to_str()?;
622        let lang = codeprism_core::ast::Language::from_extension(extension);
623        if matches!(lang, codeprism_core::ast::Language::Unknown) {
624            None
625        } else {
626            Some(lang)
627        }
628    }
629
630    /// Get server capabilities
631    pub fn capabilities(&self) -> &ServerCapabilities {
632        &self.capabilities
633    }
634
635    /// Get repository manager for accessing Phase 2.5 functionality
636    pub fn repository_manager(&self) -> &RepositoryManager {
637        &self.repository_manager
638    }
639
640    /// Get repository scanner
641    pub fn scanner(&self) -> &RepositoryScanner {
642        &self.scanner
643    }
644
645    /// Get bulk indexer
646    pub fn indexer(&self) -> &BulkIndexer {
647        &self.indexer
648    }
649
650    /// Get parser engine
651    pub fn parser_engine(&self) -> &std::sync::Arc<ParserEngine> {
652        &self.parser_engine
653    }
654
655    /// Get graph store
656    pub fn graph_store(&self) -> &Arc<GraphStore> {
657        &self.graph_store
658    }
659
660    /// Get graph query engine
661    pub fn graph_query(&self) -> &GraphQuery {
662        &self.graph_query
663    }
664
665    /// Get content search manager
666    pub fn content_search(&self) -> &Arc<codeprism_core::ContentSearchManager> {
667        &self.content_search
668    }
669
670    /// Get current repository path
671    pub fn repository_path(&self) -> Option<&Path> {
672        self.repository_path.as_deref()
673    }
674}
675
676impl Default for CodePrismMcpServer {
677    fn default() -> Self {
678        Self::new().expect("Failed to create default MCP server")
679    }
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685    use std::fs;
686    use tempfile::TempDir;
687
688    #[tokio::test]
689    async fn test_mcp_server_creation() {
690        let server = CodePrismMcpServer::new().expect("Failed to create MCP server");
691
692        // Verify capabilities are properly set
693        assert!(server.capabilities().resources.is_some());
694        assert!(server.capabilities().tools.is_some());
695        assert!(server.capabilities().prompts.is_some());
696
697        // Verify no repository is set initially
698        assert!(server.repository_path().is_none());
699    }
700
701    #[tokio::test]
702    async fn test_mcp_server_initialize_with_repository() {
703        let temp_dir = TempDir::new().expect("Failed to create temp dir");
704        let repo_path = temp_dir.path();
705
706        // Create a test file
707        fs::write(repo_path.join("test.py"), "print('hello world')").unwrap();
708
709        let mut server = CodePrismMcpServer::new().expect("Failed to create MCP server");
710        server
711            .initialize_with_repository(repo_path)
712            .await
713            .expect("Failed to initialize with repository");
714
715        // Verify repository is set
716        assert!(server.repository_path().is_some());
717        assert_eq!(server.repository_path().unwrap(), repo_path);
718    }
719
720    #[tokio::test]
721    async fn test_mcp_server_capabilities() {
722        let server = CodePrismMcpServer::new().expect("Failed to create MCP server");
723        let capabilities = server.capabilities();
724
725        // Verify resource capabilities
726        let resource_caps = capabilities.resources.as_ref().unwrap();
727        assert_eq!(resource_caps.subscribe, Some(true));
728        assert_eq!(resource_caps.list_changed, Some(true));
729
730        // Verify tool capabilities
731        let tool_caps = capabilities.tools.as_ref().unwrap();
732        assert_eq!(tool_caps.list_changed, Some(true));
733
734        // Verify prompt capabilities
735        let prompt_caps = capabilities.prompts.as_ref().unwrap();
736        assert_eq!(prompt_caps.list_changed, Some(false));
737    }
738}