Compare commits

...

1 Commits

Author SHA1 Message Date
claude[bot] c51b70a217 feat(mcp): add workspace_id parameter to create_memory_project
Adds an optional `workspace_id` parameter to the `create_memory_project`
MCP tool, which is forwarded as `workspace=` to `get_client()`. This
allows MCP callers to target a non-default cloud workspace when creating
a new project, resolving the chicken-and-egg problem where no project_id
exists yet to perform per-project routing.

When `workspace_id` is omitted the behaviour is unchanged — the
connection's default workspace is used.

Closes #788

Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-05-03 15:14:52 +00:00
2 changed files with 55 additions and 1 deletions
@@ -371,6 +371,7 @@ async def create_memory_project(
project_name: str,
project_path: str,
set_default: bool = False,
workspace_id: str | None = None,
output_format: Literal["text", "json"] = "text",
context: Context | None = None,
) -> str | dict:
@@ -383,6 +384,9 @@ async def create_memory_project(
project_name: Name for the new project (must be unique)
project_path: File system path where the project will be stored
set_default: Whether to set this project as the default (optional, defaults to False)
workspace_id: Target workspace tenant_id for cloud deployments. When omitted the
connection's default workspace is used. Discover available values via
list_workspaces() which returns tenant_id for each workspace.
output_format: "text" returns the existing human-readable result text.
"json" returns structured project creation metadata.
context: Optional FastMCP context for progress/status logging.
@@ -393,8 +397,9 @@ async def create_memory_project(
Example:
create_memory_project("my-research", "~/Documents/research")
create_memory_project("work-notes", "/home/user/work", set_default=True)
create_memory_project("team-notes", "/home/user/team", workspace_id="ws-uuid-here")
"""
async with get_client() as client:
async with get_client(workspace=workspace_id) as client:
# Check if server is constrained to a specific project
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
if constrained_project:
+49
View File
@@ -133,6 +133,55 @@ async def test_create_and_delete_project_and_name_match_branch(
assert delete_result.startswith("")
@pytest.mark.asyncio
async def test_create_memory_project_passes_workspace_id(app, tmp_path_factory):
"""workspace_id is forwarded to get_client(workspace=...) for cloud workspace targeting."""
from contextlib import asynccontextmanager
import httpx
from basic_memory.mcp.clients import ProjectClient
from basic_memory.schemas.project_info import ProjectStatusResponse
project_root = tmp_path_factory.mktemp("ws-project-home")
captured_workspace = {}
fake_list = _make_list([], default=None)
fake_status = ProjectStatusResponse(
message="Project created",
status="success",
default=False,
new_project=_make_project("WS Project", str(project_root)),
)
@asynccontextmanager
async def fake_get_client(*, workspace=None, project_name=None):
captured_workspace["workspace"] = workspace
async with httpx.AsyncClient(base_url="http://testserver") as client:
yield client
with (
patch(
"basic_memory.mcp.tools.project_management.get_client",
new=fake_get_client,
),
patch.object(ProjectClient, "list_projects", new_callable=AsyncMock, return_value=fake_list),
patch.object(
ProjectClient, "create_project", new_callable=AsyncMock, return_value=fake_status
),
patch(
"basic_memory.mcp.project_context.invalidate_workspace_project_index",
new_callable=AsyncMock,
),
):
await create_memory_project(
project_name="WS Project",
project_path=str(project_root),
workspace_id="tenant-abc-123",
)
assert captured_workspace["workspace"] == "tenant-abc-123"
# --- Cloud merge tests ---