Skip to content

Commit 05adda1

Browse files
authored
fix(mcp): route workspace-qualified memory urls (#790)
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 0a72d81 commit 05adda1

20 files changed

Lines changed: 1950 additions & 78 deletions

src/basic_memory/api/app.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from fastapi import FastAPI, HTTPException, Request
66
from fastapi.exception_handlers import http_exception_handler
7+
from fastapi.responses import JSONResponse
78
from fastapi.routing import APIRouter
89
from loguru import logger
910

@@ -29,6 +30,12 @@
2930
from basic_memory.config import init_api_logging
3031
from basic_memory.services.exceptions import EntityAlreadyExistsError
3132
from basic_memory.services.initialization import initialize_app
33+
from basic_memory.workspace_context import (
34+
WORKSPACE_SLUG_HEADER,
35+
WORKSPACE_TYPE_HEADER,
36+
workspace_permalink_context_validation_error,
37+
workspace_permalink_context,
38+
)
3239

3340

3441
@asynccontextmanager
@@ -87,6 +94,32 @@ async def lifespan(app: FastAPI): # pragma: no cover
8794
lifespan=lifespan,
8895
)
8996

97+
98+
@app.middleware("http")
99+
async def workspace_permalink_context_middleware(request: Request, call_next):
100+
"""Populate workspace permalink context from request headers."""
101+
workspace_slug = request.headers.get(WORKSPACE_SLUG_HEADER)
102+
workspace_type = request.headers.get(WORKSPACE_TYPE_HEADER)
103+
104+
validation_error = workspace_permalink_context_validation_error(workspace_slug, workspace_type)
105+
if validation_error is not None:
106+
return JSONResponse(
107+
status_code=400,
108+
content={"detail": validation_error},
109+
)
110+
111+
if not workspace_slug:
112+
return await call_next(request)
113+
114+
# ContextVar state remains active across the awaited downstream handler while
115+
# this context manager is open, so entity creation can see request metadata.
116+
with workspace_permalink_context(
117+
workspace_slug=workspace_slug,
118+
workspace_type=workspace_type,
119+
):
120+
return await call_next(request)
121+
122+
90123
# Include v2 routers FIRST (more specific paths must match before /{project} catch-all)
91124
app.include_router(v2_knowledge, prefix="/v2/projects/{project_id}")
92125
app.include_router(v2_memory, prefix="/v2/projects/{project_id}")
@@ -146,4 +179,7 @@ async def exception_handler(request, exc): # pragma: no cover
146179
error_type=type(exc).__name__,
147180
error=str(exc),
148181
)
149-
return await http_exception_handler(request, HTTPException(status_code=500, detail=str(exc)))
182+
return await http_exception_handler(
183+
request,
184+
HTTPException(status_code=500, detail="Internal server error"),
185+
)

src/basic_memory/mcp/async_client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,12 @@ async def _cloud_client(
9494
workspace: Optional[str] = None,
9595
) -> AsyncIterator[AsyncClient]:
9696
"""Create a cloud proxy client with resolved credentials."""
97+
from basic_memory.workspace_context import workspace_permalink_headers
98+
9799
token = await _resolve_cloud_token(config)
98100
proxy_base_url = f"{config.cloud_host}/proxy"
99101
headers = {"Authorization": f"Bearer {token}"}
102+
headers.update(workspace_permalink_headers())
100103
if workspace:
101104
headers["X-Workspace-ID"] = workspace
102105
logger.info(f"Creating HTTP client for cloud proxy at: {proxy_base_url}")

0 commit comments

Comments
 (0)