Skip to content

Commit 2efd8f4

Browse files
phernandezclaude
andauthored
fix: critical cloud deployment fixes for MCP stability (#317)
Signed-off-by: phernandez <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 5da97e4 commit 2efd8f4

File tree

4 files changed

+50
-9
lines changed

4 files changed

+50
-9
lines changed

src/basic_memory/mcp/async_client.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from httpx import ASGITransport, AsyncClient
2+
from httpx import ASGITransport, AsyncClient, Timeout
33
from loguru import logger
44

55
from basic_memory.api.app import app as fastapi_app
@@ -14,14 +14,25 @@ def create_client() -> AsyncClient:
1414
proxy_base_url = os.getenv("BASIC_MEMORY_PROXY_URL", None)
1515
logger.info(f"BASIC_MEMORY_PROXY_URL: {proxy_base_url}")
1616

17+
# Configure timeout for longer operations like write_note
18+
# Default httpx timeout is 5 seconds which is too short for file operations
19+
timeout = Timeout(
20+
connect=10.0, # 10 seconds for connection
21+
read=30.0, # 30 seconds for reading response
22+
write=30.0, # 30 seconds for writing request
23+
pool=30.0, # 30 seconds for connection pool
24+
)
25+
1726
if proxy_base_url:
1827
# Use HTTP transport to proxy endpoint
1928
logger.info(f"Creating HTTP client for proxy at: {proxy_base_url}")
20-
return AsyncClient(base_url=proxy_base_url)
29+
return AsyncClient(base_url=proxy_base_url, timeout=timeout)
2130
else:
2231
# Default: use ASGI transport for local API (development mode)
2332
logger.debug("Creating ASGI client for local Basic Memory API")
24-
return AsyncClient(transport=ASGITransport(app=fastapi_app), base_url="http://test")
33+
return AsyncClient(
34+
transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
35+
)
2536

2637

2738
# Create shared async client

src/basic_memory/mcp/tools/headers.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,19 @@ def inject_auth_header(headers: HeaderTypes | None = None) -> HeaderTypes:
2626
headers = headers.copy()
2727

2828
http_headers = get_http_headers()
29-
logger.debug(f"HTTP headers: {http_headers}")
29+
30+
# Log only non-sensitive header keys for debugging
31+
if logger.opt(lazy=True).debug:
32+
sensitive_headers = {"authorization", "cookie", "x-api-key", "x-auth-token", "api-key"}
33+
safe_headers = {k for k in http_headers.keys() if k.lower() not in sensitive_headers}
34+
logger.debug(f"HTTP headers present: {list(safe_headers)}")
3035

3136
authorization = http_headers.get("Authorization") or http_headers.get("authorization")
3237
if authorization:
3338
headers["Authorization"] = authorization # type: ignore
34-
logger.debug("Injected JWT token into authorization request headers")
39+
# Log only that auth was injected, not the token value
40+
logger.debug("Injected authorization header into request")
3541
else:
36-
logger.debug("No authorization found in request headers")
42+
logger.debug("No authorization header found in request")
3743

3844
return headers

src/basic_memory/mcp/tools/read_note.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ async def read_note(
130130
query=identifier, search_type="title", project=project, context=context
131131
)
132132

133-
if title_results and title_results.results:
133+
# Handle both SearchResponse object and error strings
134+
if title_results and hasattr(title_results, "results") and title_results.results:
134135
result = title_results.results[0] # Get the first/best match
135136
if result.permalink:
136137
try:
@@ -159,7 +160,8 @@ async def read_note(
159160
)
160161

161162
# We didn't find a direct match, construct a helpful error message
162-
if not text_results or not text_results.results:
163+
# Handle both SearchResponse object and error strings
164+
if not text_results or not hasattr(text_results, "results") or not text_results.results:
163165
# No results at all
164166
return format_not_found_message(active_project.name, identifier)
165167
else:

tests/api/test_async_client.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tests for async_client configuration."""
22

33
from unittest.mock import patch
4-
from httpx import AsyncClient, ASGITransport
4+
from httpx import AsyncClient, ASGITransport, Timeout
55

66
from basic_memory.mcp.async_client import create_client
77

@@ -25,3 +25,25 @@ def test_create_client_uses_http_when_proxy_env_set():
2525
assert not isinstance(client._transport, ASGITransport)
2626
# When using remote API, no base_url is set (dynamic from headers)
2727
assert str(client.base_url) == "http://localhost:8000"
28+
29+
30+
def test_create_client_configures_extended_timeouts():
31+
"""Test that create_client configures 30-second timeouts for long operations."""
32+
with patch.dict("os.environ", {}, clear=True):
33+
client = create_client()
34+
35+
# Verify timeout configuration
36+
assert isinstance(client.timeout, Timeout)
37+
assert client.timeout.connect == 10.0 # 10 seconds for connection
38+
assert client.timeout.read == 30.0 # 30 seconds for reading
39+
assert client.timeout.write == 30.0 # 30 seconds for writing
40+
assert client.timeout.pool == 30.0 # 30 seconds for pool
41+
42+
# Also test with proxy URL
43+
with patch.dict("os.environ", {"BASIC_MEMORY_PROXY_URL": "http://localhost:8000"}):
44+
client = create_client()
45+
46+
# Same timeout configuration should apply
47+
assert isinstance(client.timeout, Timeout)
48+
assert client.timeout.read == 30.0
49+
assert client.timeout.write == 30.0

0 commit comments

Comments
 (0)