response and backoff fixes
This commit is contained in:
parent
a7c073909a
commit
50b7d08bb2
|
|
@ -6,6 +6,7 @@ with proper error handling, retries, and timeout management.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
@ -82,7 +83,7 @@ class NoteDiscoveryClient:
|
||||||
method=method,
|
method=method,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Execute with retries
|
# Execute with retries and exponential backoff
|
||||||
last_error = None
|
last_error = None
|
||||||
for attempt in range(self.config.max_retries):
|
for attempt in range(self.config.max_retries):
|
||||||
try:
|
try:
|
||||||
|
|
@ -94,7 +95,7 @@ class NoteDiscoveryClient:
|
||||||
status_code=response.status,
|
status_code=response.status,
|
||||||
)
|
)
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
# HTTP error (4xx, 5xx)
|
# HTTP error (4xx, 5xx) - don't retry, return immediately
|
||||||
error_body = ""
|
error_body = ""
|
||||||
try:
|
try:
|
||||||
error_body = e.read().decode("utf-8")
|
error_body = e.read().decode("utf-8")
|
||||||
|
|
@ -109,11 +110,16 @@ class NoteDiscoveryClient:
|
||||||
status_code=e.code,
|
status_code=e.code,
|
||||||
)
|
)
|
||||||
except urllib.error.URLError as e:
|
except urllib.error.URLError as e:
|
||||||
# Network error - retry
|
# Network error - retry with backoff
|
||||||
last_error = f"Connection error: {e.reason}"
|
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
|
continue
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
|
# Timeout - retry with backoff
|
||||||
last_error = f"Request timed out after {self.timeout}s"
|
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
|
continue
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
return APIResponse(
|
return APIResponse(
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,35 @@ class MCPServer:
|
||||||
# Tool Implementations
|
# 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:
|
def _format_response(self, response: APIResponse) -> str:
|
||||||
"""Format API response as readable text."""
|
"""Format API response as readable text."""
|
||||||
if not response.success:
|
if not response.success:
|
||||||
|
|
@ -242,8 +271,9 @@ class MCPServer:
|
||||||
def _tool_get_note(self, args: dict) -> str:
|
def _tool_get_note(self, args: dict) -> str:
|
||||||
"""Get note content."""
|
"""Get note content."""
|
||||||
path = args.get("path", "")
|
path = args.get("path", "")
|
||||||
if not path:
|
is_valid, error = self._validate_path(path)
|
||||||
return "Error: path is required"
|
if not is_valid:
|
||||||
|
return f"Error: {error}"
|
||||||
|
|
||||||
response = self.client.get_note(path)
|
response = self.client.get_note(path)
|
||||||
|
|
||||||
|
|
@ -341,8 +371,9 @@ class MCPServer:
|
||||||
path = args.get("path", "")
|
path = args.get("path", "")
|
||||||
content = args.get("content", "")
|
content = args.get("content", "")
|
||||||
|
|
||||||
if not path:
|
is_valid, error = self._validate_path(path)
|
||||||
return "Error: path is required"
|
if not is_valid:
|
||||||
|
return f"Error: {error}"
|
||||||
if not content:
|
if not content:
|
||||||
return "Error: content is required"
|
return "Error: content is required"
|
||||||
|
|
||||||
|
|
@ -356,8 +387,9 @@ class MCPServer:
|
||||||
def _tool_delete_note(self, args: dict) -> str:
|
def _tool_delete_note(self, args: dict) -> str:
|
||||||
"""Delete a note."""
|
"""Delete a note."""
|
||||||
path = args.get("path", "")
|
path = args.get("path", "")
|
||||||
if not path:
|
is_valid, error = self._validate_path(path)
|
||||||
return "Error: path is required"
|
if not is_valid:
|
||||||
|
return f"Error: {error}"
|
||||||
|
|
||||||
response = self.client.delete_note(path)
|
response = self.client.delete_note(path)
|
||||||
|
|
||||||
|
|
@ -369,8 +401,9 @@ class MCPServer:
|
||||||
def _tool_create_folder(self, args: dict) -> str:
|
def _tool_create_folder(self, args: dict) -> str:
|
||||||
"""Create a folder."""
|
"""Create a folder."""
|
||||||
path = args.get("path", "")
|
path = args.get("path", "")
|
||||||
if not path:
|
is_valid, error = self._validate_path(path)
|
||||||
return "Error: path is required"
|
if not is_valid:
|
||||||
|
return f"Error: {error}"
|
||||||
|
|
||||||
response = self.client.create_folder(path)
|
response = self.client.create_folder(path)
|
||||||
|
|
||||||
|
|
@ -402,8 +435,10 @@ class MCPServer:
|
||||||
def _tool_get_template(self, args: dict) -> str:
|
def _tool_get_template(self, args: dict) -> str:
|
||||||
"""Get template content."""
|
"""Get template content."""
|
||||||
name = args.get("name", "")
|
name = args.get("name", "")
|
||||||
if not name:
|
# Validate template name (same rules as paths for safety)
|
||||||
return "Error: name is required"
|
is_valid, error = self._validate_path(name)
|
||||||
|
if not is_valid:
|
||||||
|
return f"Error: {error.replace('path', 'name')}"
|
||||||
|
|
||||||
response = self.client.get_template(name)
|
response = self.client.get_template(name)
|
||||||
|
|
||||||
|
|
@ -450,10 +485,22 @@ class MCPServer:
|
||||||
# Notifications don't get responses
|
# Notifications don't get responses
|
||||||
|
|
||||||
elif method == "tools/list":
|
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)
|
result = self.handle_list_tools(params)
|
||||||
self._send_response(request_id, result)
|
self._send_response(request_id, result)
|
||||||
|
|
||||||
elif method == "tools/call":
|
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)
|
result = self.handle_call_tool(params)
|
||||||
self._send_response(request_id, result)
|
self._send_response(request_id, result)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue