Skip to content

Commit db5014f

Browse files
phernandezclaude
andcommitted
fix(mcp): pass workspace parameter through client factory
The factory mode (used by cloud MCP server) silently dropped the workspace parameter, making per-tool workspace switching impossible. Every MCP tool already accepts a workspace parameter and passes it to get_project_client(), but in factory mode the value was discarded. - Update factory type from Callable[[], ...] to Callable[..., ...] to accept optional workspace kwarg - Pass workspace through get_client() to the factory - Pass workspace through get_project_client() in factory mode - Update test factories to accept workspace parameter Closes basicmachines-co/basic-memory-cloud#460 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 367fcaa commit db5014f

File tree

4 files changed

+15
-12
lines changed

4 files changed

+15
-12
lines changed

src/basic_memory/mcp/async_client.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,14 @@ async def get_cloud_control_plane_client(
128128
yield client
129129

130130

131-
# Optional factory override for dependency injection
132-
_client_factory: Optional[Callable[[], AbstractAsyncContextManager[AsyncClient]]] = None
131+
# Optional factory override for dependency injection.
132+
# The factory accepts an optional workspace keyword argument so that MCP tools
133+
# can route individual requests to a different workspace than the one set at
134+
# connection time. See basic-memory-cloud main.py tenant_asgi_client_factory.
135+
_client_factory: Optional[Callable[..., AbstractAsyncContextManager[AsyncClient]]] = None
133136

134137

135-
def set_client_factory(factory: Callable[[], AbstractAsyncContextManager[AsyncClient]]) -> None:
138+
def set_client_factory(factory: Callable[..., AbstractAsyncContextManager[AsyncClient]]) -> None:
136139
"""Override the default client factory (for cloud app, testing, etc)."""
137140
global _client_factory
138141
_client_factory = factory
@@ -173,7 +176,7 @@ async def get_client(
173176
4. Local ASGI transport by default.
174177
"""
175178
if _client_factory:
176-
async with _client_factory() as client:
179+
async with _client_factory(workspace=workspace) as client:
177180
yield client
178181
return
179182

src/basic_memory/mcp/project_context.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -622,10 +622,10 @@ async def get_project_client(
622622

623623
# Step 1b: Factory injection (in-process cloud server)
624624
# Trigger: set_client_factory() was called (e.g., by cloud MCP server)
625-
# Why: the transport layer already resolved workspace and tenant context;
626-
# attempting cloud workspace resolution here would call the production
627-
# control-plane API with no valid credentials and fail with 401
628-
# Outcome: use the factory client directly, skip workspace resolution
625+
# Why: the factory's transport layer handles auth and tenant resolution;
626+
# we pass workspace through so the transport can route to the correct
627+
# workspace when the tool specifies one different from the connection default
628+
# Outcome: factory client with optional workspace override via inner request headers
629629
if is_factory_mode():
630630
route_mode = "factory"
631631
with telemetry.scope(
@@ -635,7 +635,7 @@ async def get_project_client(
635635
workspace_id=workspace,
636636
):
637637
logger.debug("Using injected client factory for project routing")
638-
async with get_client() as client:
638+
async with get_client(workspace=workspace) as client:
639639
active_project = await get_active_project(client, resolved_project, context)
640640
yield client, active_project
641641
return

tests/mcp/test_async_client_modes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async def test_get_client_uses_injected_factory(monkeypatch):
2828
seen = {"used": False}
2929

3030
@asynccontextmanager
31-
async def factory():
31+
async def factory(workspace=None):
3232
seen["used"] = True
3333
async with httpx.AsyncClient(base_url="https://example.test") as client:
3434
yield client
@@ -185,7 +185,7 @@ async def test_get_client_factory_overrides_per_project_routing(config_manager):
185185
config_manager.save_config(cfg)
186186

187187
@asynccontextmanager
188-
async def factory():
188+
async def factory(workspace=None):
189189
async with httpx.AsyncClient(base_url="https://factory.test") as client:
190190
yield client
191191

tests/mcp/test_project_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ async def test_factory_mode_skips_workspace_resolution(self, config_manager, mon
709709

710710
# Set up a factory (simulates what cloud MCP server does)
711711
@asynccontextmanager
712-
async def fake_factory():
712+
async def fake_factory(workspace=None):
713713
from httpx import ASGITransport, AsyncClient
714714
from basic_memory.api.app import app as fastapi_app
715715

0 commit comments

Comments
 (0)