Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/basic_memory/mcp/async_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from httpx import ASGITransport, AsyncClient
from httpx import ASGITransport, AsyncClient, Timeout
from loguru import logger

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

# Configure timeout for longer operations like write_note
# Default httpx timeout is 5 seconds which is too short for file operations
timeout = Timeout(
connect=10.0, # 10 seconds for connection
read=30.0, # 30 seconds for reading response
write=30.0, # 30 seconds for writing request
pool=30.0, # 30 seconds for connection pool
)

if proxy_base_url:
# Use HTTP transport to proxy endpoint
logger.info(f"Creating HTTP client for proxy at: {proxy_base_url}")
return AsyncClient(base_url=proxy_base_url)
return AsyncClient(base_url=proxy_base_url, timeout=timeout)
else:
# Default: use ASGI transport for local API (development mode)
logger.debug("Creating ASGI client for local Basic Memory API")
return AsyncClient(transport=ASGITransport(app=fastapi_app), base_url="http://test")
return AsyncClient(
transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
)


# Create shared async client
Expand Down
12 changes: 9 additions & 3 deletions src/basic_memory/mcp/tools/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@ def inject_auth_header(headers: HeaderTypes | None = None) -> HeaderTypes:
headers = headers.copy()

http_headers = get_http_headers()
logger.debug(f"HTTP headers: {http_headers}")

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

authorization = http_headers.get("Authorization") or http_headers.get("authorization")
if authorization:
headers["Authorization"] = authorization # type: ignore
logger.debug("Injected JWT token into authorization request headers")
# Log only that auth was injected, not the token value
logger.debug("Injected authorization header into request")
else:
logger.debug("No authorization found in request headers")
logger.debug("No authorization header found in request")

return headers
6 changes: 4 additions & 2 deletions src/basic_memory/mcp/tools/read_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ async def read_note(
query=identifier, search_type="title", project=project, context=context
)

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

# We didn't find a direct match, construct a helpful error message
if not text_results or not text_results.results:
# Handle both SearchResponse object and error strings
if not text_results or not hasattr(text_results, "results") or not text_results.results:
# No results at all
return format_not_found_message(active_project.name, identifier)
else:
Expand Down
24 changes: 23 additions & 1 deletion tests/api/test_async_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Tests for async_client configuration."""

from unittest.mock import patch
from httpx import AsyncClient, ASGITransport
from httpx import AsyncClient, ASGITransport, Timeout

from basic_memory.mcp.async_client import create_client

Expand All @@ -25,3 +25,25 @@ def test_create_client_uses_http_when_proxy_env_set():
assert not isinstance(client._transport, ASGITransport)
# When using remote API, no base_url is set (dynamic from headers)
assert str(client.base_url) == "http://localhost:8000"


def test_create_client_configures_extended_timeouts():
"""Test that create_client configures 30-second timeouts for long operations."""
with patch.dict("os.environ", {}, clear=True):
client = create_client()

# Verify timeout configuration
assert isinstance(client.timeout, Timeout)
assert client.timeout.connect == 10.0 # 10 seconds for connection
assert client.timeout.read == 30.0 # 30 seconds for reading
assert client.timeout.write == 30.0 # 30 seconds for writing
assert client.timeout.pool == 30.0 # 30 seconds for pool

# Also test with proxy URL
with patch.dict("os.environ", {"BASIC_MEMORY_PROXY_URL": "http://localhost:8000"}):
client = create_client()

# Same timeout configuration should apply
assert isinstance(client.timeout, Timeout)
assert client.timeout.read == 30.0
assert client.timeout.write == 30.0
Loading