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