diff --git a/mcp_server/client.py b/mcp_server/client.py index 37391b1..e1ce7c1 100644 --- a/mcp_server/client.py +++ b/mcp_server/client.py @@ -6,6 +6,7 @@ with proper error handling, retries, and timeout management. """ import json +import time import urllib.request import urllib.error import urllib.parse @@ -82,7 +83,7 @@ class NoteDiscoveryClient: method=method, ) - # Execute with retries + # Execute with retries and exponential backoff last_error = None for attempt in range(self.config.max_retries): try: @@ -94,7 +95,7 @@ class NoteDiscoveryClient: status_code=response.status, ) except urllib.error.HTTPError as e: - # HTTP error (4xx, 5xx) + # HTTP error (4xx, 5xx) - don't retry, return immediately error_body = "" try: error_body = e.read().decode("utf-8") @@ -109,11 +110,16 @@ class NoteDiscoveryClient: status_code=e.code, ) except urllib.error.URLError as e: - # Network error - retry + # Network error - retry with backoff last_error = f"Connection error: {e.reason}" + if attempt < self.config.max_retries - 1: + time.sleep(2 ** attempt * 0.1) # 0.1s, 0.2s, 0.4s... continue except TimeoutError: + # Timeout - retry with backoff last_error = f"Request timed out after {self.timeout}s" + if attempt < self.config.max_retries - 1: + time.sleep(2 ** attempt * 0.1) # 0.1s, 0.2s, 0.4s... continue except json.JSONDecodeError as e: return APIResponse( diff --git a/mcp_server/server.py b/mcp_server/server.py index 8dc734a..55f1ed1 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -164,6 +164,35 @@ class MCPServer: # Tool Implementations # ========================================================================= + def _validate_path(self, path: str) -> tuple[bool, str]: + """ + Validate a path for safety. + + Args: + path: The path to validate + + Returns: + Tuple of (is_valid, error_message) + """ + if not path: + return False, "path is required" + + # Reject path traversal attempts + if ".." in path: + return False, "path cannot contain '..'" + + # Reject absolute paths (Unix and Windows) + if path.startswith("/") or path.startswith("\\"): + return False, "path cannot be absolute" + if len(path) >= 2 and path[1] == ":": # Windows drive letter (e.g., C:) + return False, "path cannot be absolute" + + # Reject null bytes (security) + if "\x00" in path: + return False, "path contains invalid characters" + + return True, "" + def _format_response(self, response: APIResponse) -> str: """Format API response as readable text.""" if not response.success: @@ -242,8 +271,9 @@ class MCPServer: def _tool_get_note(self, args: dict) -> str: """Get note content.""" path = args.get("path", "") - if not path: - return "Error: path is required" + is_valid, error = self._validate_path(path) + if not is_valid: + return f"Error: {error}" response = self.client.get_note(path) @@ -341,8 +371,9 @@ class MCPServer: path = args.get("path", "") content = args.get("content", "") - if not path: - return "Error: path is required" + is_valid, error = self._validate_path(path) + if not is_valid: + return f"Error: {error}" if not content: return "Error: content is required" @@ -356,8 +387,9 @@ class MCPServer: def _tool_delete_note(self, args: dict) -> str: """Delete a note.""" path = args.get("path", "") - if not path: - return "Error: path is required" + is_valid, error = self._validate_path(path) + if not is_valid: + return f"Error: {error}" response = self.client.delete_note(path) @@ -369,8 +401,9 @@ class MCPServer: def _tool_create_folder(self, args: dict) -> str: """Create a folder.""" path = args.get("path", "") - if not path: - return "Error: path is required" + is_valid, error = self._validate_path(path) + if not is_valid: + return f"Error: {error}" response = self.client.create_folder(path) @@ -402,8 +435,10 @@ class MCPServer: def _tool_get_template(self, args: dict) -> str: """Get template content.""" name = args.get("name", "") - if not name: - return "Error: name is required" + # Validate template name (same rules as paths for safety) + is_valid, error = self._validate_path(name) + if not is_valid: + return f"Error: {error.replace('path', 'name')}" response = self.client.get_template(name) @@ -450,10 +485,22 @@ class MCPServer: # Notifications don't get responses elif method == "tools/list": + if not self._initialized: + self._send_response( + request_id, + error=self._error(-32002, "Server not initialized") + ) + return result = self.handle_list_tools(params) self._send_response(request_id, result) elif method == "tools/call": + if not self._initialized: + self._send_response( + request_id, + error=self._error(-32002, "Server not initialized") + ) + return result = self.handle_call_tool(params) self._send_response(request_id, result)