From 662828fc5753beff49c3a337b32dc1b0931cf8ba Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 04:03:08 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`feature?= =?UTF-8?q?/workflow-improvements`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @rishitank. * https://github.com/rishitank/context-engine/pull/1#issuecomment-3704422544 The following files were modified: * `src/mcp/progress.rs` * `src/mcp/prompts.rs` * `src/mcp/resources.rs` * `src/mcp/server.rs` * `src/tools/mod.rs` * `src/tools/navigation.rs` * `src/tools/workspace.rs` --- src/mcp/progress.rs | 157 ++++++++++++++- src/mcp/prompts.rs | 71 ++++++- src/mcp/resources.rs | 148 +++++++++++++- src/mcp/server.rs | 414 +++++++++++++++++++++++++++++++++++++--- src/tools/mod.rs | 21 +- src/tools/navigation.rs | 258 ++++++++++++++++++++++++- src/tools/workspace.rs | 400 +++++++++++++++++++++++++++++++++++++- 7 files changed, 1411 insertions(+), 58 deletions(-) diff --git a/src/mcp/progress.rs b/src/mcp/progress.rs index 7d5b364..ebbbfb0 100644 --- a/src/mcp/progress.rs +++ b/src/mcp/progress.rs @@ -34,7 +34,23 @@ pub struct ProgressNotification { } impl ProgressNotification { - /// Create a new progress notification. + /// Constructs a JSON-RPC progress notification containing the provided token, progress value, optional total, and optional message. + /// + /// # Examples + /// + /// ``` + /// let note = ProgressNotification::new( + /// ProgressToken::String("op-1".into()), + /// 50, + /// Some(100), + /// Some("in progress".into()), + /// ); + /// assert_eq!(note.jsonrpc, "2.0"); + /// assert_eq!(note.method, "notifications/progress"); + /// assert_eq!(note.params.progress, 50); + /// assert_eq!(note.params.total, Some(100)); + /// assert_eq!(note.params.message.as_deref(), Some("in progress")); + /// ``` pub fn new( token: ProgressToken, progress: u64, @@ -63,7 +79,17 @@ pub struct ProgressReporter { } impl ProgressReporter { - /// Create a new progress reporter. + /// Constructs a ProgressReporter bound to a progress token, a sender channel, and an optional total. + /// + /// # Examples + /// + /// ``` + /// use tokio::sync::mpsc; + /// use crate::mcp::progress::{ProgressReporter, ProgressToken}; + /// + /// let (tx, _rx) = mpsc::channel(1); + /// let reporter = ProgressReporter::new(ProgressToken::Number(1), tx, Some(100)); + /// ``` pub fn new( token: ProgressToken, sender: mpsc::Sender, @@ -76,7 +102,21 @@ impl ProgressReporter { } } - /// Report progress. + /// Send a progress notification for this reporter. + /// + /// The optional `message`, if provided, is included in the notification. Send failures are ignored. + /// + /// # Examples + /// + /// ``` + /// # use futures::executor::block_on; + /// # use crate::mcp::progress::{ProgressManager, ProgressToken}; + /// let manager = ProgressManager::new(); + /// let reporter = manager.create_reporter(Some(100)); + /// block_on(async { + /// reporter.report(42, Some("halfway")).await; + /// }); + /// ``` pub async fn report(&self, progress: u64, message: Option<&str>) { let notification = ProgressNotification::new( self.token.clone(), @@ -87,7 +127,20 @@ impl ProgressReporter { let _ = self.sender.send(notification).await; } - /// Report progress with percentage. + /// Converts a percentage into an absolute progress value (using the reporter's `total` when present) and emits that progress notification. + /// + /// # Examples + /// + /// ``` + /// # use tokio::sync::mpsc; + /// # use crate::mcp::progress::{ProgressManager}; + /// # #[tokio::test] + /// # async fn example_report_percent() { + /// let manager = ProgressManager::new(); + /// let reporter = manager.create_reporter(Some(200)); + /// reporter.report_percent(50, Some("Halfway")).await; + /// # } + /// ``` pub async fn report_percent(&self, percent: u64, message: Option<&str>) { let progress = if let Some(total) = self.total { (percent * total) / 100 @@ -97,7 +150,26 @@ impl ProgressReporter { self.report(progress, message).await; } - /// Complete the progress. + /// Report completion for this reporter by sending a notification with progress set to the reporter's total, if one is configured. + /// + /// If the reporter has no configured total, no notification is sent. + /// + /// # Parameters + /// + /// - `message`: Optional message to include with the completion notification. + /// + /// # Examples + /// + /// ``` + /// use tokio::sync::mpsc; + /// use crate::mcp::progress::{ProgressReporter, ProgressToken}; + /// + /// // Create a reporter with a total of 100 and send completion. + /// let rt = tokio::runtime::Runtime::new().unwrap(); + /// let (tx, _rx) = mpsc::channel(10); + /// let reporter = ProgressReporter::new(ProgressToken::Number(1), tx, Some(100)); + /// rt.block_on(reporter.complete(Some("finished"))); + /// ``` pub async fn complete(&self, message: Option<&str>) { if let Some(total) = self.total { self.report(total, message).await; @@ -113,7 +185,15 @@ pub struct ProgressManager { } impl ProgressManager { - /// Create a new progress manager. + /// Creates a new ProgressManager configured to emit progress notifications. + /// + /// # Examples + /// + /// ``` + /// let mgr = ProgressManager::new(); + /// // obtain a receiver to consume notifications + /// let _recv = mgr.receiver(); + /// ``` pub fn new() -> Self { let (sender, receiver) = mpsc::channel(100); Self { @@ -123,7 +203,26 @@ impl ProgressManager { } } - /// Create a new progress reporter with a generated token. + /// Creates a new ProgressReporter that uses a generated numeric token. + /// + /// The generated token is a sequential numeric identifier unique to this ProgressManager instance. + /// + /// # Parameters + /// + /// - `total`: Optional total number of work units for the operation; if provided, percentage-based reporting + /// will be computed against this value. + /// + /// # Returns + /// + /// A `ProgressReporter` bound to this manager's sender, using a newly generated numeric `ProgressToken`. + /// + /// # Examples + /// + /// ``` + /// let manager = ProgressManager::new(); + /// let reporter = manager.create_reporter(Some(100)); + /// // `reporter` can now be used to emit progress updates. + /// ``` pub fn create_reporter(&self, total: Option) -> ProgressReporter { let id = self .next_id @@ -132,7 +231,19 @@ impl ProgressManager { ProgressReporter::new(token, self.sender.clone(), total) } - /// Create a progress reporter with a specific token. + /// Creates a ProgressReporter bound to the given token and optional total. + /// + /// The returned reporter will send progress notifications tagged with `token` + /// using the manager's internal channel. + /// + /// # Examples + /// + /// ``` + /// use crate::mcp::progress::{ProgressManager, ProgressToken}; + /// + /// let manager = ProgressManager::new(); + /// let reporter = manager.create_reporter_with_token(ProgressToken::String("op".into()), Some(100)); + /// ``` pub fn create_reporter_with_token( &self, token: ProgressToken, @@ -141,13 +252,39 @@ impl ProgressManager { ProgressReporter::new(token, self.sender.clone(), total) } - /// Get the receiver for progress notifications. + /// Returns a clone of the shared receiver handle for progress notifications. + /// + /// The returned `Arc>>` can be cloned and used by consumers to lock and receive progress notifications. + /// + /// # Examples + /// + /// ``` + /// let manager = ProgressManager::new(); + /// let rx = manager.receiver(); + /// // `rx` is a clone of the manager's shared receiver handle + /// assert!(Arc::strong_count(&rx) >= 1); + /// ``` pub fn receiver(&self) -> Arc>> { self.receiver.clone() } } impl Default for ProgressManager { + /// Creates a ProgressManager initialized with its standard channel and token counter. + + /// + + /// # Examples + + /// + + /// ``` + + /// let mgr = crate::mcp::progress::ProgressManager::default(); + + /// let _recv = mgr.receiver(); + + /// ``` fn default() -> Self { Self::new() } @@ -263,4 +400,4 @@ mod tests { assert!(json.contains("\"total\":100")); assert!(json.contains("\"message\":\"Working...\"")); } -} +} \ No newline at end of file diff --git a/src/mcp/prompts.rs b/src/mcp/prompts.rs index f8e326c..b7975a8 100644 --- a/src/mcp/prompts.rs +++ b/src/mcp/prompts.rs @@ -71,14 +71,32 @@ pub struct PromptTemplate { } impl PromptRegistry { - /// Create a new registry with built-in prompts. + /// Creates a new registry populated with the built-in prompts. + /// + /// # Examples + /// + /// ``` + /// let registry = crate::mcp::prompts::PromptRegistry::new(); + /// assert!(!registry.list().is_empty()); + /// ``` pub fn new() -> Self { let mut registry = Self::default(); registry.register_builtin_prompts(); registry } - /// Register built-in prompts. + /// Populates the registry with the built-in prompt definitions used by the application. + /// + /// Registers three prompts — "code_review", "explain_code", and "write_tests" — each with their + /// argument metadata and template text (including conditional sections and variable placeholders). + /// + /// # Examples + /// + /// ``` + /// let registry = crate::mcp::prompts::PromptRegistry::new(); + /// let names: Vec<_> = registry.list().into_iter().map(|p| p.name).collect(); + /// assert!(names.contains(&"code_review".to_string())); + /// ``` fn register_builtin_prompts(&mut self) { // Code Review Prompt self.register( @@ -191,17 +209,58 @@ Include: ); } - /// Register a prompt. + /// Adds or updates a prompt and its template in the registry. + /// + /// The provided `prompt` is stored under its `name`; if a prompt with the same name + /// already exists it will be replaced along with its template. + /// + /// # Examples + /// + /// ``` + /// let mut registry = PromptRegistry::new(); + /// let prompt = Prompt { + /// name: "example".to_string(), + /// description: "An example prompt".to_string(), + /// arguments: vec![], + /// }; + /// let template = PromptTemplate { template: "Hello {{name}}".to_string() }; + /// registry.register(prompt, template); + /// assert!(registry.list().iter().any(|p| p.name == "example")); + /// ``` pub fn register(&mut self, prompt: Prompt, template: PromptTemplate) { self.prompts.insert(prompt.name.clone(), (prompt, template)); } - /// List all prompts. + /// Retrieve all registered prompts. + /// + /// Returns a vector containing a clone of each registered `Prompt`. The order of prompts is not guaranteed. + /// + /// # Examples + /// + /// ``` + /// let registry = PromptRegistry::new(); + /// let prompts = registry.list(); + /// assert!(prompts.iter().any(|p| p.name == "code_review")); + /// ``` pub fn list(&self) -> Vec { self.prompts.values().map(|(p, _)| p.clone()).collect() } - /// Get a prompt by name with arguments substituted. + /// Retrieve a registered prompt by name and render its template using the provided arguments. + /// + /// The template supports conditional blocks of the form `{{#if var}}...{{/if}}` (the block is included only when `var` is present and not empty) and simple `{{variable}}` substitutions. Any remaining unsubstituted placeholders are removed from the output. Returns `None` if no prompt with the given name exists. On success the result contains the prompt description and a single user-role message with the rendered text. + /// + /// # Examples + /// + /// ``` + /// use std::collections::HashMap; + /// + /// let registry = PromptRegistry::new(); + /// let mut args = HashMap::new(); + /// args.insert("code".to_string(), "fn main() {}".to_string()); + /// let res = registry.get("code_review", &args); + /// assert!(res.is_some()); + /// ``` pub fn get(&self, name: &str, arguments: &HashMap) -> Option { self.prompts.get(name).map(|(prompt, template)| { let mut text = template.template.clone(); @@ -369,4 +428,4 @@ mod tests { assert!(!text.contains("{{#if")); assert!(!text.contains("{{/if}}")); } -} +} \ No newline at end of file diff --git a/src/mcp/resources.rs b/src/mcp/resources.rs index b7363fd..692d5f6 100644 --- a/src/mcp/resources.rs +++ b/src/mcp/resources.rs @@ -59,7 +59,22 @@ pub struct ResourceRegistry { } impl ResourceRegistry { - /// Create a new resource registry. + /// Creates a new ResourceRegistry backed by the given workspace context. + /// + /// # Parameters + /// + /// - `context_service`: shared workspace context used to resolve the workspace root and related operations. + /// + /// # Examples + /// + /// ``` + /// use std::sync::Arc; + /// # use crate::mcp::resources::ResourceRegistry; + /// # use crate::context::ContextService; + /// // Construct or obtain an Arc from your application. + /// let ctx: Arc = Arc::new(ContextService::default()); + /// let registry = ResourceRegistry::new(ctx); + /// ``` pub fn new(context_service: Arc) -> Self { Self { context_service, @@ -67,7 +82,24 @@ impl ResourceRegistry { } } - /// List available resources (files in workspace). + /// Lists workspace files as `Resource` entries with optional cursor-based pagination. + /// + /// The `cursor` parameter, if provided, is a resource name to start listing after; results include up to 100 entries. + /// + /// # Returns + /// + /// `ListResourcesResult` containing the discovered resources and an optional `next_cursor` string to continue pagination. + /// + /// # Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// // `registry` would be constructed with a real `ContextService` in production. + /// // let registry = ResourceRegistry::new(context_service); + /// // let result = registry.list(None).await.unwrap(); + /// // assert!(result.resources.len() <= 100); + /// # }); + /// ``` pub async fn list(&self, cursor: Option<&str>) -> Result { let workspace = self.context_service.workspace(); let files = self.discover_files(workspace, 100, cursor).await?; @@ -107,7 +139,34 @@ impl ResourceRegistry { }) } - /// Read a resource by URI. + /// Reads a resource identified by a `file://` URI from the workspace and returns its contents. + /// + /// # Arguments + /// + /// * `uri` - A `file://` URI pointing to a file located inside the workspace. + /// + /// # Returns + /// + /// A `ReadResourceResult` containing a single `ResourceContents` entry with the provided `uri`, the inferred `mime_type` (if any), and `text` set to the file's UTF-8 contents. + /// + /// # Errors + /// + /// Returns `Error::InvalidToolArguments` when: + /// - the URI does not start with `file://`, + /// - the workspace or target path cannot be canonicalized, + /// - the resolved path is outside the workspace, or + /// - the file cannot be read. + /// + /// # Examples + /// + /// ``` + /// # async fn example_usage(registry: &crate::mcp::resources::ResourceRegistry) -> anyhow::Result<()> { + /// let result = registry.read("file:///path/to/workspace/file.txt").await?; + /// assert_eq!(result.contents.len(), 1); + /// let content = &result.contents[0]; + /// assert_eq!(content.uri, "file:///path/to/workspace/file.txt"); + /// # Ok(()) } + /// ``` pub async fn read(&self, uri: &str) -> Result { // Parse file:// URI let path = if let Some(path) = uri.strip_prefix("file://") { @@ -151,7 +210,28 @@ impl ResourceRegistry { }) } - /// Subscribe to resource changes. + /// Registers a session to receive change notifications for the given resource URI. + /// + /// The session ID will be recorded in the registry's in-memory subscription map for the specified URI. + /// + /// # Parameters + /// + /// - `uri`: The resource URI to subscribe to (e.g., a `file://` URI). + /// - `session_id`: The identifier of the session to register for notifications. + /// + /// # Returns + /// + /// `Ok(())` on success. + /// + /// # Examples + /// + /// ```no_run + /// # use std::sync::Arc; + /// # use tokio::runtime::Runtime; + /// # async fn _example(registry: &crate::mcp::resources::ResourceRegistry) { + /// registry.subscribe("file:///path/to/file", "session-123").await.unwrap(); + /// # } + /// ``` pub async fn subscribe(&self, uri: &str, session_id: &str) -> Result<()> { let mut subs = self.subscriptions.write().await; subs.entry(uri.to_string()) @@ -160,7 +240,20 @@ impl ResourceRegistry { Ok(()) } - /// Unsubscribe from resource changes. + /// Remove a session's subscription for the given resource URI. + /// + /// # Examples + /// + /// ``` + /// # use std::sync::Arc; + /// # use tokio::runtime::Runtime; + /// # // Assume `registry` is an initialized `ResourceRegistry`. + /// # let rt = Runtime::new().unwrap(); + /// # rt.block_on(async { + /// let registry = /* ResourceRegistry instance */ unimplemented!(); + /// registry.unsubscribe("file:///path/to/file", "session-123").await.unwrap(); + /// # }); + /// ``` pub async fn unsubscribe(&self, uri: &str, session_id: &str) -> Result<()> { let mut subs = self.subscriptions.write().await; if let Some(sessions) = subs.get_mut(uri) { @@ -230,7 +323,19 @@ impl ResourceRegistry { Ok(files) } - /// Check if a file should be ignored. + /// Returns whether a file or directory name matches common ignore patterns used when discovering files. + /// + /// Matches directory names: "node_modules", "target", "dist", "build", "__pycache__", ".git", + /// and files whose names end with `.lock` or `.pyc`. + /// + /// # Examples + /// + /// ``` + /// assert!(should_ignore("node_modules")); + /// assert!(should_ignore("Cargo.lock")); + /// assert!(should_ignore("__pycache__")); + /// assert!(!should_ignore("src")); + /// ``` fn should_ignore(name: &str) -> bool { matches!( name, @@ -239,8 +344,19 @@ impl ResourceRegistry { || name.ends_with(".pyc") } - /// Convert a path to a proper file:// URI. - /// Handles Windows paths by converting backslashes and adding leading slash. + /// Convert a filesystem path to a file:// URI. + /// + /// On Windows this replaces backslashes with forward slashes and prefixes + /// absolute drive paths (e.g., `C:/path`) with `file:///`. On other platforms + /// the path is prefixed with `file://`. + /// + /// # Examples + /// + /// ``` + /// use std::path::Path; + /// let uri = path_to_file_uri(Path::new("/some/path")); + /// assert!(uri.starts_with("file://")); + /// ``` fn path_to_file_uri(path: &std::path::Path) -> String { let path_str = path.to_string_lossy(); @@ -262,7 +378,19 @@ impl ResourceRegistry { } } - /// Guess MIME type from file extension. + /// Infer a MIME type string for a file path based on its extension. + /// + /// Returns `Some` with a guessed MIME type for known extensions, `Some("text/plain")` for unknown extensions, + /// and `None` if the path has no extension or the extension is not valid UTF-8. + /// + /// # Examples + /// + /// ``` + /// use std::path::Path; + /// assert_eq!(guess_mime_type(Path::new("main.rs")), Some("text/x-rust".to_string())); + /// assert_eq!(guess_mime_type(Path::new("data.unknown")), Some("text/plain".to_string())); + /// assert_eq!(guess_mime_type(Path::new("no_extension")), None); + /// ``` fn guess_mime_type(path: &std::path::Path) -> Option { let ext = path.extension()?.to_str()?; let mime = match ext { @@ -396,4 +524,4 @@ mod tests { assert_eq!(parsed.contents.len(), 1); assert_eq!(parsed.contents[0].text, Some("code".to_string())); } -} +} \ No newline at end of file diff --git a/src/mcp/server.rs b/src/mcp/server.rs index 21cb44d..4cde8d5 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -31,6 +31,20 @@ pub enum LogLevel { } impl LogLevel { + /// Converts a case-insensitive string into the corresponding `LogLevel`, defaulting to `Info` for unknown values. + /// + /// # Returns + /// The matching `LogLevel` variant; `Info` if the input is not recognized. + /// + /// # Examples + /// + /// ``` + /// use crate::mcp::server::LogLevel; + /// + /// assert_eq!(LogLevel::from_str("debug"), LogLevel::Debug); + /// assert_eq!(LogLevel::from_str("Warn"), LogLevel::Warning); + /// assert_eq!(LogLevel::from_str("unknown-level"), LogLevel::Info); + /// ``` fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { "debug" => Self::Debug, @@ -45,6 +59,17 @@ impl LogLevel { } } + /// Get the lowercase string name for the log level. + /// + /// The returned string is a static, lowercase identifier corresponding to the variant + /// (for example, `"info"`, `"warning"`, or `"error"`). + /// + /// # Examples + /// + /// ``` + /// let lvl = LogLevel::Info; + /// assert_eq!(lvl.as_str(), "info"); + /// ``` fn as_str(&self) -> &'static str { match self { Self::Debug => "debug", @@ -77,7 +102,18 @@ pub struct McpServer { } impl McpServer { - /// Create a new MCP server. + /// Creates a new MCP server with default features. + /// + /// The returned server uses an empty prompt registry, no resource registry (resources disabled), + /// empty workspace roots, no active or cancelled requests, and the default log level and version. + /// + /// # Examples + /// + /// ``` + /// // create a handler appropriate for your setup + /// let handler = /* create or obtain an McpHandler instance */ ; + /// let _server = McpServer::new(handler, "my-server"); + /// ``` pub fn new(handler: McpHandler, name: impl Into) -> Self { Self { handler: Arc::new(handler), @@ -92,7 +128,20 @@ impl McpServer { } } - /// Create a new MCP server with all features. + /// Create a McpServer configured with prompts and an initialized resources registry. + /// + /// The returned server wraps the provided handler and prompt registry in Arcs, + /// constructs a ResourceRegistry from `context_service`, and initializes + /// empty workspace roots, active/cancelled request tracking, and the default log level. + /// + /// # Examples + /// + /// ```ignore + /// use std::sync::Arc; + /// + /// // Assume `handler`, `prompts`, and `context_service` are available. + /// let server = McpServer::with_features(handler, prompts, Arc::new(context_service), "my-server"); + /// ``` pub fn with_features( handler: McpHandler, prompts: PromptRegistry, @@ -112,38 +161,124 @@ impl McpServer { } } - /// Get the current log level. + /// Retrieve the server's current log level. + /// + /// # Returns + /// + /// `LogLevel` containing the server's active log level. + /// + /// # Examples + /// + /// ``` + /// # use futures::executor::block_on; + /// # // `server` must be a `McpServer` instance + /// # let server = todo!(); + /// let level = block_on(server.log_level()); + /// ``` pub async fn log_level(&self) -> LogLevel { *self.log_level.read().await } - /// Set the log level. + /// Update the server's current logging level. + /// + /// This changes the level that the server uses for subsequent log messages. + /// + /// # Examples + /// + /// ``` + /// # use crate::mcp::server::{McpServer, LogLevel}; + /// # async fn doc_example(server: &McpServer) { + /// server.set_log_level(LogLevel::Debug).await; + /// # } + /// ``` pub async fn set_log_level(&self, level: LogLevel) { *self.log_level.write().await = level; } - /// Get the client-provided workspace roots. + /// Retrieve the client-provided workspace roots. + /// + /// # Examples + /// + /// ```no_run + /// // Obtain an McpServer instance from your application context. + /// let server: McpServer = unimplemented!(); + /// + /// // Call the async method to get the current roots. + /// let roots = futures::executor::block_on(server.roots()); + /// assert!(roots.iter().all(|p| p.is_absolute())); + /// ``` pub async fn roots(&self) -> Vec { self.roots.read().await.clone() } - /// Check if a request has been explicitly cancelled. + /// Returns whether the given request ID has been explicitly cancelled. + + /// + + /// # Examples + + /// + + /// ``` + + /// // Assuming `server: McpServer` and `id: RequestId` are available: + + /// // let cancelled = server.is_cancelled(&id).await; + + /// ``` pub async fn is_cancelled(&self, id: &RequestId) -> bool { self.cancelled_requests.read().await.contains(id) } - /// Mark a request as cancelled. + /// Marks the given request ID as cancelled so the server will treat it as cancelled on subsequent checks. + /// + /// # Examples + /// + /// ``` + /// // Assuming `server` is an instance of `McpServer` and `req_id` is a `RequestId`: + /// // server.cancel_request(&req_id).await; + /// ``` pub async fn cancel_request(&self, id: &RequestId) { self.cancelled_requests.write().await.insert(id.clone()); } - /// Clean up a completed request from tracking sets. + /// Remove a request from the server's active and cancelled tracking sets. + /// + /// This removes `id` from both `active_requests` and `cancelled_requests`, ensuring + /// the server no longer treats the request as in-progress or cancelled. + /// + /// # Examples + /// + /// ```no_run + /// # use mcp::server::McpServer; + /// # use mcp::RequestId; + /// # async fn example(server: &McpServer, id: &RequestId) { + /// server.complete_request(id).await; + /// # } + /// ``` pub async fn complete_request(&self, id: &RequestId) { self.active_requests.write().await.remove(id); self.cancelled_requests.write().await.remove(id); } - /// Run the server with the given transport. + /// Run the server loop that processes incoming MCP messages on the provided transport. + /// + /// Starts the transport, receives messages until the transport ends or a send failure occurs, + /// dispatches requests and notifications to the server handlers, stops the transport, and returns + /// when the server has shut down. + /// + /// # Returns + /// + /// `Ok(())` on normal shutdown; an `Err` is returned if starting or stopping the transport fails. + /// + /// # Examples + /// + /// ``` + /// # use std::sync::Arc; + /// # async fn _example(server: Arc, transport: impl crate::transport::Transport) { + /// server.run(transport).await.unwrap(); + /// # } + /// ``` pub async fn run(&self, mut transport: T) -> Result<()> { info!("Starting MCP server: {} v{}", self.name, self.version); @@ -172,7 +307,21 @@ impl McpServer { Ok(()) } - /// Handle a JSON-RPC request. + /// Dispatches an incoming JSON-RPC request to the appropriate handler, tracks the request lifecycle for cancellation, and returns the corresponding JSON-RPC response. + /// + /// The request is registered as active while being processed; upon completion it is removed from active tracking. Known MCP methods are routed to their specific handlers; unknown methods produce a protocol error encoded in the response. + /// + /// # Returns + /// + /// `JsonRpcResponse` containing either a successful `result` value or an `error` describing the failure. + /// + /// # Examples + /// + /// ```no_run + /// // `server` and `request` are assumed to be initialized appropriately. + /// let resp = futures::executor::block_on(server.handle_request(request)); + /// assert_eq!(resp.jsonrpc, "2.0"); + /// ``` async fn handle_request(&self, req: JsonRpcRequest) -> JsonRpcResponse { debug!("Handling request: {} (id: {:?})", req.method, req.id); @@ -228,7 +377,29 @@ impl McpServer { } } - /// Handle a notification. + /// Process an incoming JSON-RPC notification and perform any side effects for known notification types. + /// + /// Known notifications handled: + /// - "notifications/initialized": logs client initialization. + /// - "notifications/cancelled": extracts a `requestId` from `params` and marks the request cancelled. + /// - "notifications/roots/listChanged": logs that client workspace roots changed. + /// Unknown notifications are ignored (logged at debug level). + /// + /// # Examples + /// + /// ```no_run + /// use serde_json::json; + /// + /// // Build a cancelled notification with a `requestId` param. + /// let notif = JsonRpcNotification { + /// jsonrpc: "2.0".into(), + /// method: "notifications/cancelled".into(), + /// params: Some(json!({ "requestId": "some-request-id" })), + /// }; + /// + /// // `server` is an instance of `McpServer`. Call will mark the request cancelled. + /// // server.handle_notification(notif).await; + /// ``` async fn handle_notification(&self, notif: JsonRpcNotification) { debug!("Handling notification: {}", notif.method); @@ -259,7 +430,20 @@ impl McpServer { } } - /// Handle initialize request. + /// Build and return the server's initialize result as JSON. + /// + /// If `params` includes client workspace roots with URIs beginning with `file://`, + /// those paths are added to the server's tracked roots. The returned JSON contains + /// the protocol version, server capabilities (including resources capability only + /// if resources support is enabled), and server info (name and version). + /// + /// # Examples + /// + /// ``` + /// // Call on a server instance: returns an `InitializeResult` serialized as JSON. + /// // let resp = server.handle_initialize(None).await.unwrap(); + /// // assert!(resp.get("protocol_version").is_some()); + /// ``` async fn handle_initialize(&self, params: Option) -> Result { // Extract roots from client if provided if let Some(ref params) = params { @@ -322,7 +506,29 @@ impl McpServer { Ok(serde_json::to_value(result)?) } - /// Handle call tool request. + /// Calls a named tool with the supplied parameters and returns the tool's result as JSON. + /// + /// Expects `params` to be a JSON-encoded `CallToolParams` object containing the tool `name` and `arguments`. + /// + /// # Returns + /// + /// The tool's execution result as a `serde_json::Value`. + /// + /// # Errors + /// + /// Returns `Error::InvalidToolArguments` if `params` is missing or cannot be deserialized into `CallToolParams`, + /// `Error::ToolNotFound` if no tool with the given name is registered, and propagates errors from the tool's + /// execution or JSON serialization. + /// + /// # Examples + /// + /// ``` + /// use serde_json::json; + /// + /// // Example params: { "name": "echo", "arguments": ["hello"] } + /// let params = Some(json!({ "name": "echo", "arguments": ["hello"] })); + /// // let result = server.handle_call_tool(params).await.unwrap(); + /// ``` async fn handle_call_tool(&self, params: Option) -> Result { let params: CallToolParams = params .ok_or_else(|| Error::InvalidToolArguments("Missing params".to_string())) @@ -339,7 +545,22 @@ impl McpServer { Ok(serde_json::to_value(result)?) } - /// Handle list prompts request. + /// List available prompts and return them as a JSON value. + /// + /// The returned JSON matches `ListPromptsResult` with the `prompts` field populated + /// and `next_cursor` set to `null`. + /// + /// # Examples + /// + /// ``` + /// # use crate::mcp::prompts::ListPromptsResult; + /// # tokio_test::block_on(async { + /// // assume `server` is a constructed `McpServer` + /// let json = server.handle_list_prompts().await.unwrap(); + /// let res: ListPromptsResult = serde_json::from_value(json).unwrap(); + /// assert!(res.next_cursor.is_none()); + /// # }); + /// ``` async fn handle_list_prompts(&self) -> Result { use crate::mcp::prompts::ListPromptsResult; @@ -351,7 +572,26 @@ impl McpServer { Ok(serde_json::to_value(result)?) } - /// Handle get prompt request. + /// Fetches a prompt by name with optional arguments and returns it as JSON. + /// + /// Expects `params` to be a JSON object with a required `name` string and an optional + /// `arguments` object mapping strings to strings. Returns the prompt result serialized + /// to a `serde_json::Value`. + /// + /// Errors: + /// - Returns `Error::InvalidToolArguments` if `params` is missing or cannot be deserialized. + /// - Returns `Error::McpProtocol` if no prompt with the given name exists. + /// + /// # Examples + /// + /// ``` + /// # use serde_json::json; + /// # async fn _example(server: &crate::mcp::server::McpServer) { + /// let params = json!({ "name": "welcome", "arguments": { "user": "Alex" } }); + /// let res = server.handle_get_prompt(Some(params)).await.unwrap(); + /// // `res` is a serde_json::Value containing the prompt result + /// # } + /// ``` async fn handle_get_prompt(&self, params: Option) -> Result { #[derive(serde::Deserialize)] struct GetPromptParams { @@ -374,7 +614,26 @@ impl McpServer { Ok(serde_json::to_value(result)?) } - /// Handle list resources request. + /// Lists available resources using an optional pagination cursor. + /// + /// If the server was built without resource support this returns an MCP protocol + /// error indicating resources are not enabled. When resources are enabled, the + /// optional `params` JSON may contain a `"cursor"` string used for paging; the + /// function returns the serialized listing result from the resource registry. + /// + /// # Errors + /// + /// Returns `Error::McpProtocol("Resources not enabled")` if resources are not + /// configured for the server, or propagates errors from the resource registry + /// or JSON serialization. + /// + /// # Examples + /// + /// ``` + /// // Construct the optional params JSON with a cursor: + /// let params = serde_json::json!({ "cursor": "page-2" }); + /// // Call: server.handle_list_resources(Some(params)).await + /// ``` async fn handle_list_resources(&self, params: Option) -> Result { let resources = self .resources @@ -394,7 +653,30 @@ impl McpServer { Ok(serde_json::to_value(result)?) } - /// Handle read resource request. + /// Read a resource identified by a URI and return its serialized content as JSON. + /// + /// Returns an error if resources are not enabled, if required parameters are missing or malformed, + /// or if the underlying resource read operation fails. + /// + /// # Examples + /// + /// ``` + /// # use serde_json::json; + /// # use std::sync::Arc; + /// # async fn _example(server: &crate::mcp::server::McpServer) { + /// let params = json!({ "uri": "file:///path/to/resource" }); + /// let result = server.handle_read_resource(Some(params)).await; + /// match result { + /// Ok(value) => { + /// // `value` is the JSON-serialized content returned by the resource registry. + /// println!("{}", value); + /// } + /// Err(e) => { + /// eprintln!("read failed: {:?}", e); + /// } + /// } + /// # } + /// ``` async fn handle_read_resource(&self, params: Option) -> Result { let resources = self .resources @@ -416,7 +698,25 @@ impl McpServer { Ok(serde_json::to_value(result)?) } - /// Handle subscribe to resource. + /// Subscribe the default session to a resource identified by URI. + /// + /// Returns an error if resources are not enabled for this server or if the required `params` are + /// missing or cannot be deserialized. + /// + /// The request causes the server to call the configured ResourceRegistry's `subscribe` method for + /// the provided URI using a placeholder session id ("default") and, on success, returns an empty + /// JSON object. + /// + /// # Examples + /// + /// ```no_run + /// # use serde_json::json; + /// # async fn example(server: &crate::mcp::McpServer) -> Result<(), Box> { + /// let params = json!({ "uri": "file:///path/to/resource" }); + /// let res = server.handle_subscribe_resource(Some(params)).await?; + /// assert_eq!(res, json!({})); + /// # Ok(()) } + /// ``` async fn handle_subscribe_resource(&self, params: Option) -> Result { let resources = self .resources @@ -461,7 +761,33 @@ impl McpServer { Ok(serde_json::json!({})) } - /// Handle completion request. + /// Provide completion suggestions for a completion request. + /// + /// Expects `params` to deserialize to `{ ref: { type, uri?, name? }, argument: { name, value } }`. + /// For argument names "path", "file", or "uri" it returns filesystem/resource path completions; + /// for argument name "prompt" when `ref.type == "ref/prompt"` it returns prompt-name completions. + /// The response is a JSON object with a `completion` field containing `values` (an array of strings) + /// and `hasMore` (a boolean). + /// + /// # Examples + /// + /// ```no_run + /// use serde_json::json; + /// + /// // Example request params for completing prompt names starting with "ins" + /// let params = json!({ + /// "ref": { "type": "ref/prompt" }, + /// "argument": { "name": "prompt", "value": "ins" } + /// }); + /// + /// // Expected shape of the response: + /// let expected = json!({ + /// "completion": { + /// "values": ["install", "instance"], // example values + /// "hasMore": false + /// } + /// }); + /// ``` async fn handle_completion(&self, params: Option) -> Result { #[derive(serde::Deserialize)] struct CompletionParams { @@ -517,7 +843,26 @@ impl McpServer { })) } - /// Complete file paths. + /// Generates file-path completion candidates that start with the given prefix. + /// + /// The returned completions are sourced from the optional resource registry (if enabled) + /// and from files/directories under client-provided workspace roots. Results are + /// deduplicated and limited to at most 20 entries. + /// + /// # Returns + /// + /// A vector of completion strings that begin with `prefix`, up to 20 items. + /// + /// # Examples + /// + /// ``` + /// // `server` is an instance of `McpServer`. + /// // This example assumes an async context (e.g., inside an async test). + /// # async fn example(server: &crate::mcp::server::McpServer) { + /// let completions = server.complete_file_path("src/").await; + /// // completions contains candidates like "src/main.rs", "src/lib.rs", ... + /// # } + /// ``` async fn complete_file_path(&self, prefix: &str) -> Vec { let roots = self.roots.read().await; let mut completions = Vec::new(); @@ -559,7 +904,32 @@ impl McpServer { completions.into_iter().take(20).collect() } - /// Handle logging/setLevel request. + /// Set the server's log level from RPC parameters. + /// + /// Expects `params` to be a JSON object `{ "level": "" }`. Parses the `level` string, + /// updates the server's log level, logs the change, and returns an empty JSON object on success. + /// If `params` is `None`, returns an MCP protocol error indicating the missing parameter. + /// Unknown or unrecognized level strings map to the default level (Info). + /// + /// # Parameters + /// + /// - `params`: Optional JSON `Value` containing a `level` string specifying the desired log level. + /// + /// # Returns + /// + /// An empty JSON object `{}` on success. + /// + /// # Examples + /// + /// ``` + /// # async fn docs_example(server: &McpServer) { + /// let res = server + /// .handle_set_log_level(Some(serde_json::json!({ "level": "debug" }))) + /// .await + /// .unwrap(); + /// assert_eq!(res, serde_json::json!({})); + /// # } + /// ``` async fn handle_set_log_level(&self, params: Option) -> Result { #[derive(serde::Deserialize)] struct SetLevelParams { @@ -610,4 +980,4 @@ mod tests { fn test_log_level_default() { assert_eq!(LogLevel::default(), LogLevel::Info); } -} +} \ No newline at end of file diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 20a6d74..f2ee572 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -23,7 +23,24 @@ use std::sync::Arc; use crate::mcp::handler::McpHandler; use crate::service::{ContextService, MemoryService, PlanningService}; -/// Register all tools with the handler. +/// Registers the built-in MCP tools with the given handler using the provided services. +/// +/// The function registers a fixed set of tools organized by category (retrieval, index, +/// memory, planning, review, navigation, and workspace), constructing each tool with the +/// appropriate service(s) supplied. +/// +/// # Examples +/// +/// ``` +/// use std::sync::Arc; +/// +/// let mut handler = McpHandler::new(); +/// let ctx = Arc::new(ContextService::default()); +/// let mem = Arc::new(MemoryService::default()); +/// let plan = Arc::new(PlanningService::default()); +/// +/// register_all_tools(&mut handler, ctx, mem, plan); +/// ``` pub fn register_all_tools( handler: &mut McpHandler, context_service: Arc, @@ -102,4 +119,4 @@ pub fn register_all_tools( handler.register(workspace::WorkspaceStatsTool::new(context_service.clone())); handler.register(workspace::GitStatusTool::new(context_service.clone())); handler.register(workspace::ExtractSymbolsTool::new(context_service)); -} +} \ No newline at end of file diff --git a/src/tools/navigation.rs b/src/tools/navigation.rs index dacf215..11a0038 100644 --- a/src/tools/navigation.rs +++ b/src/tools/navigation.rs @@ -19,6 +19,18 @@ pub struct FindReferencesTool { } impl FindReferencesTool { + /// Creates a new instance of the tool that shares the provided context service. + /// + /// The `service` is held by the tool and used to access workspace state and perform + /// file search, definition lookup, or diff operations depending on the tool. + /// + /// # Examples + /// + /// ``` + /// use std::sync::Arc; + /// let service = Arc::new(ContextService::new()); + /// let tool = FindReferencesTool::new(service.clone()); + /// ``` pub fn new(service: Arc) -> Self { Self { service } } @@ -26,6 +38,23 @@ impl FindReferencesTool { #[async_trait] impl ToolHandler for FindReferencesTool { + /// Returns the tool descriptor for the "find_references" tool, describing its name, + /// purpose, and expected input schema. + /// + /// The returned Tool has: + /// - name: "find_references" + /// - description: brief explanation of the tool's purpose (searches for symbol usages) + /// - input_schema: JSON schema requiring `symbol` and optionally accepting `file_pattern` + /// and `max_results` (default: 50). + /// + /// # Examples + /// + /// ``` + /// // Construct the tool descriptor and verify its name. + /// let svc = Arc::new(ContextService::new()); // pseudo-code: supply a real service in use + /// let tool = FindReferencesTool::new(svc).definition(); + /// assert_eq!(tool.name, "find_references"); + /// ``` fn definition(&self) -> Tool { Tool { name: "find_references".to_string(), @@ -51,6 +80,27 @@ impl ToolHandler for FindReferencesTool { } } + /// Finds occurrences of a symbol across the workspace and returns a Markdown-formatted summary of matches. + /// + /// If any references are found, the result contains a Markdown document with a header and a bullet list + /// of file paths, line numbers, and line context for each occurrence. If no references are found, the + /// result contains a success message stating that no references were discovered for the requested symbol. + /// + /// # Returns + /// + /// A `ToolResult` containing either the Markdown list of references or a success message indicating no references. + /// + /// # Examples + /// + /// ``` + /// # use std::collections::HashMap; + /// # use serde_json::json; + /// # use futures::executor::block_on; + /// # // assuming `tool` is an instance of the tool in a test setup + /// let mut args = HashMap::new(); + /// args.insert("symbol".to_string(), json!("my_symbol")); + /// // block_on(tool.execute(args)) // -> ToolResult with Markdown or "No references found..." + /// ``` async fn execute(&self, args: HashMap) -> Result { let symbol = get_string_arg(&args, "symbol")?; let file_pattern = args.get("file_pattern").and_then(|v| v.as_str()); @@ -94,6 +144,18 @@ pub struct GoToDefinitionTool { } impl GoToDefinitionTool { + /// Creates a new instance of the tool that shares the provided context service. + /// + /// The `service` is held by the tool and used to access workspace state and perform + /// file search, definition lookup, or diff operations depending on the tool. + /// + /// # Examples + /// + /// ``` + /// use std::sync::Arc; + /// let service = Arc::new(ContextService::new()); + /// let tool = FindReferencesTool::new(service.clone()); + /// ``` pub fn new(service: Arc) -> Self { Self { service } } @@ -101,6 +163,18 @@ impl GoToDefinitionTool { #[async_trait] impl ToolHandler for GoToDefinitionTool { + /// Creates a Tool descriptor for the "go_to_definition" tool used to locate a symbol's definition. + /// + /// The returned `Tool` includes the tool name, a brief description, and an input JSON schema + /// that requires a `symbol` and optionally accepts a `language` hint (e.g., "rust", "python"). + /// + /// # Examples + /// + /// ```no_run + /// // Obtain the descriptor from a GoToDefinitionTool instance: + /// let tool = GoToDefinitionTool::new(std::sync::Arc::new(context_service)).definition(); + /// assert_eq!(tool.name, "go_to_definition"); + /// ``` fn definition(&self) -> Tool { Tool { name: "go_to_definition".to_string(), @@ -122,6 +196,31 @@ impl ToolHandler for GoToDefinitionTool { } } + /// Finds definitions for the provided symbol in the workspace and returns a Markdown document + /// describing each match with file path, line number, and a fenced code snippet tagged with the detected language. + /// + /// The `args` map must contain the key `"symbol"` with the symbol name to search for. It may also + /// include an optional `"language"` string to hint which language to prefer when locating definitions. + /// + /// # Returns + /// + /// A `ToolResult` containing a Markdown-formatted document listing each definition found. If no + /// definitions are found, the result contains a plain message stating that no definition was found. + /// + /// # Examples + /// + /// ```no_run + /// use std::collections::HashMap; + /// use serde_json::json; + /// + /// // `tool` is assumed to be an instance implementing this `execute` method. + /// let mut args = HashMap::new(); + /// args.insert("symbol".to_string(), json!("my_function")); + /// // Optionally: args.insert("language".to_string(), json!("rust")); + /// + /// // let result = tool.execute(args).await.unwrap(); + /// // println!("{}", result); + /// ``` async fn execute(&self, args: HashMap) -> Result { let symbol = get_string_arg(&args, "symbol")?; let language = args.get("language").and_then(|v| v.as_str()); @@ -154,6 +253,18 @@ pub struct DiffFilesTool { } impl DiffFilesTool { + /// Creates a new instance of the tool that shares the provided context service. + /// + /// The `service` is held by the tool and used to access workspace state and perform + /// file search, definition lookup, or diff operations depending on the tool. + /// + /// # Examples + /// + /// ``` + /// use std::sync::Arc; + /// let service = Arc::new(ContextService::new()); + /// let tool = FindReferencesTool::new(service.clone()); + /// ``` pub fn new(service: Arc) -> Self { Self { service } } @@ -161,6 +272,14 @@ impl DiffFilesTool { #[async_trait] impl ToolHandler for DiffFilesTool { + /// Provides the Tool descriptor for the "diff_files" tool which compares two files and produces a unified diff. + /// + /// The descriptor includes the tool name, a short description, and an input JSON schema that requires `file1` and `file2` + /// and accepts an optional `context_lines` integer to control the number of surrounding context lines (default: 3). + /// + /// # Returns + /// + /// A `Tool` value describing the "diff_files" tool, its description, and its input schema. fn definition(&self) -> Tool { Tool { name: "diff_files".to_string(), @@ -188,6 +307,26 @@ impl ToolHandler for DiffFilesTool { } } + /// Compute a unified-style diff for two files in the workspace and return it as a tool result. + /// + /// If both files are readable and identical, the result contains the message "Files are identical.". + /// If they differ, the result contains a markdown-formatted diff wrapped in ```diff fences. + /// If either file cannot be read, the result is an error ToolResult describing the read failure. + /// + /// # Examples + /// + /// ```no_run + /// # use std::collections::HashMap; + /// # use serde_json::json; + /// # async fn example(tool: &crate::tools::navigation::DiffFilesTool) { + /// let mut args = HashMap::new(); + /// args.insert("file1".to_string(), json!("Cargo.toml")); + /// args.insert("file2".to_string(), json!("Cargo.lock")); + /// // optional: args.insert("context_lines".to_string(), json!(5)); + /// let result = tool.execute(args).await.unwrap(); + /// // Inspect `result` to see the diff or an error message. + /// # } + /// ``` async fn execute(&self, args: HashMap) -> Result { let file1 = get_string_arg(&args, "file1")?; let file2 = get_string_arg(&args, "file2")?; @@ -235,7 +374,30 @@ struct Definition { language: String, } -/// Find symbol references in files. +/// Search the workspace for occurrences of a symbol and collect matching references. +/// +/// Searches files under `workspace`, optionally filtering files by `file_pattern`, +/// and returns up to `max_results` matches as `Reference` entries containing the +/// relative file path, 1-based line number, and the matching line as context. +/// +/// # Parameters +/// +/// - `file_pattern`: optional pattern to restrict searched files (supports suffix like `"*.rs"` or substring matching). +/// - `max_results`: maximum number of references to return; search stops once this limit is reached. +/// +/// # Returns +/// +/// A `Vec` containing one entry per found occurrence, in discovery order. +/// +/// # Examples +/// +/// ``` +/// # use std::path::Path; +/// # use tokio_test::block_on; +/// // Search the current directory for the string "main", returning at most 5 matches. +/// let refs = block_on(async { crate::tools::navigation::find_symbol_in_files(Path::new("."), "main", None, 5).await }); +/// assert!(refs.len() <= 5); +/// ``` async fn find_symbol_in_files( workspace: &Path, symbol: &str, @@ -314,7 +476,22 @@ async fn find_symbol_in_files( references } -/// Find symbol definition. +/// Searches the workspace for likely definitions of `symbol` and returns any matches found. +/// +/// If `language` is provided, the search is limited to files whose detected language matches the hint +/// (for example `"rust"`, `"python"`, `"typescript"`). Each returned `Definition` contains the +/// relative file path, a 1-based line number, a short context snippet (up to a few lines), and the +/// detected language for the file. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// // Run the async function in a simple executor for the example. +/// let defs = futures::executor::block_on(find_definition(Path::new("path/to/workspace"), "my_symbol", None)); +/// // `defs` is a Vec; check if any definitions were found. +/// assert!(defs.is_empty() || defs.iter().all(|d| d.context.len() > 0)); +/// ``` async fn find_definition( workspace: &Path, symbol: &str, @@ -392,7 +569,35 @@ async fn find_definition( definitions } -/// Get definition patterns for a symbol. +/// Build a list of textual patterns commonly used to identify symbol definitions. + +/// + +/// The `symbol` is inserted into language-specific declaration snippets. The optional + +/// `language` hint restricts patterns to that language when possible; otherwise a generic + +/// set of patterns for several common languages is returned. + +/// + +/// # Examples + +/// + +/// ``` + +/// let pats = get_definition_patterns("my_fn", Some("rust")); + +/// assert!(pats.iter().any(|p| p == "fn my_fn(")); + +/// + +/// let generic = get_definition_patterns("Thing", None); + +/// assert!(generic.iter().any(|p| p.contains("class Thing") || p.contains("struct Thing"))); + +/// ``` fn get_definition_patterns(symbol: &str, language: Option<&str>) -> Vec { let mut patterns = Vec::new(); @@ -434,7 +639,17 @@ fn get_definition_patterns(symbol: &str, language: Option<&str>) -> Vec patterns } -/// Get language from file extension. +/// Map a file extension to a canonical language identifier. +/// +/// Recognizes common source file extensions and returns a short language name; unknown extensions return `"text"`. +/// +/// # Examples +/// +/// ``` +/// assert_eq!(get_language("rs"), "rust"); +/// assert_eq!(get_language("tsx"), "typescript"); +/// assert_eq!(get_language("unknown"), "text"); +/// ``` fn get_language(ext: &str) -> &'static str { match ext { "rs" => "rust", @@ -450,7 +665,18 @@ fn get_language(ext: &str) -> &'static str { } } -/// Simple pattern matching. +/// Checks whether a filename matches a simple pattern. +/// +/// Patterns starting with `"*."` are treated as extension matches (e.g., `"*.rs"` +/// matches `"foo.rs"`). All other patterns are matched by substring containment. +/// +/// # Examples +/// +/// ``` +/// assert!(matches_pattern("src/lib.rs", "*.rs")); +/// assert!(matches_pattern("README.md", "README")); +/// assert!(!matches_pattern("src/main.c", "*.rs")); +/// ``` fn matches_pattern(name: &str, pattern: &str) -> bool { if let Some(ext) = pattern.strip_prefix("*.") { name.ends_with(&format!(".{}", ext)) @@ -459,7 +685,25 @@ fn matches_pattern(name: &str, pattern: &str) -> bool { } } -/// Generate a simple unified diff. +/// Produces a unified-diff-like string describing differences between two file contents. +/// +/// The output starts with unified diff headers for `name1` and `name2` and contains one or more +/// hunks with context lines, removals marked with `-` and additions with `+`. If the contents +/// are identical, an empty string is returned. +/// +/// `context` controls how many unchanged lines around a change are included in each hunk. +/// +/// # Examples +/// +/// ``` +/// let a = "a\nb\nc\n"; +/// let b = "a\nB\nc\n"; +/// let diff = generate_diff("old.txt", "new.txt", a, b, 1); +/// assert!(diff.contains("--- old.txt")); +/// assert!(diff.contains("+++ new.txt")); +/// assert!(diff.contains("-b")); +/// assert!(diff.contains("+B")); +/// ``` fn generate_diff( name1: &str, name2: &str, @@ -638,4 +882,4 @@ mod tests { assert_eq!(definition.file, "src/lib.rs"); assert_eq!(definition.language, "rust"); } -} +} \ No newline at end of file diff --git a/src/tools/workspace.rs b/src/tools/workspace.rs index 5aa1220..2fc6494 100644 --- a/src/tools/workspace.rs +++ b/src/tools/workspace.rs @@ -19,6 +19,16 @@ pub struct WorkspaceStatsTool { } impl WorkspaceStatsTool { + /// Create a new WorkspaceStatsTool that uses the given ContextService. + /// + /// # Examples + /// + /// ```no_run + /// use std::sync::Arc; + /// // `service` should be an initialized `ContextService` from the application. + /// let service: Arc = Arc::new(/* ... */); + /// let tool = WorkspaceStatsTool::new(service); + /// ``` pub fn new(service: Arc) -> Self { Self { service } } @@ -26,6 +36,17 @@ impl WorkspaceStatsTool { #[async_trait] impl ToolHandler for WorkspaceStatsTool { + /// Returns the tool descriptor for the `workspace_stats` tool. + /// + /// The descriptor includes the tool's name, a short description of what it provides, + /// and the JSON input schema (optionally accepts `include_hidden: bool`). + /// + /// # Examples + /// + /// ``` + /// let tool = WorkspaceStatsTool::new(service).definition(); + /// assert_eq!(tool.name, "workspace_stats"); + /// ``` fn definition(&self) -> Tool { Tool { name: "workspace_stats".to_string(), @@ -43,6 +64,32 @@ impl ToolHandler for WorkspaceStatsTool { } } + /// Execute the workspace statistics tool with the given arguments. + /// + /// The `args` map may include an optional `"include_hidden"` boolean; when `true` hidden files and + /// directories are included in the statistics. On success this returns a `ToolResult` containing a + /// pretty-printed JSON string of workspace statistics (total files, total lines, per-language + /// breakdown, and directory count). On failure this returns an error `ToolResult` with a + /// descriptive message. + /// + /// # Parameters + /// + /// - `args`: A map of input arguments; recognizes the optional `"include_hidden"` boolean. + /// + /// # Examples + /// + /// ``` + /// use std::collections::HashMap; + /// use serde_json::json; + /// + /// // prepare args to include hidden files + /// let mut args = HashMap::new(); + /// args.insert("include_hidden".to_string(), json!(true)); + /// + /// // assume `tool` is an initialized `WorkspaceStatsTool` + /// // let result = tool.execute(args).await.unwrap(); + /// // println!("{}", result); + /// ``` async fn execute(&self, args: HashMap) -> Result { let include_hidden = args .get("include_hidden") @@ -73,6 +120,28 @@ struct LanguageStats { lines: usize, } +/// Collects aggregated statistics for the workspace rooted at `root`. +/// +/// Scans files and directories under `root` to compute total files, total lines, +/// a per-language breakdown (files and lines), and the number of directories encountered. +/// When `include_hidden` is `true`, hidden files and directories (those starting with a dot) +/// are included in the scan; otherwise they are skipped. +/// +/// # Examples +/// +/// ```no_run +/// # async fn example() -> anyhow::Result<()> { +/// use std::path::Path; +/// let stats = collect_workspace_stats(Path::new("."), false).await?; +/// // stats contains totals and per-language breakdowns +/// assert!(stats.total_files >= 0); +/// # Ok(()) } +/// ``` +/// +/// # Returns +/// +/// A `WorkspaceStats` value containing totals for files and lines, a language map with +/// per-language file/line counts, and the number of directories scanned. async fn collect_workspace_stats(root: &Path, include_hidden: bool) -> Result { let mut stats = WorkspaceStats { total_files: 0, @@ -85,6 +154,32 @@ async fn collect_workspace_stats(root: &Path, include_hidden: bool) -> Result