response and backoff fixes

This commit is contained in:
Gamosoft 2026-03-13 18:01:48 +01:00
parent a7c073909a
commit 50b7d08bb2
2 changed files with 66 additions and 13 deletions

View File

@ -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(

View File

@ -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)