Skip to content

fix(mcp): list factory projects across workspaces#778

Merged
phernandez merged 1 commit intomainfrom
codex-684-list-projects-workspaces
May 2, 2026
Merged

fix(mcp): list factory projects across workspaces#778
phernandez merged 1 commit intomainfrom
codex-684-list-projects-workspaces

Conversation

@phernandez
Copy link
Copy Markdown
Member

Why

Cloud MCP factory mode currently lists projects by calling the default workspace client once. That hides projects in team/organization workspaces, so callers cannot discover the external_id UUIDs they need for cross-workspace project routing.

What Changed

  • Changed list_memory_projects factory mode to use the existing workspace project index.
  • Return project entries from every accessible workspace via _merge_workspace_projects.
  • Preserve JSON default_project only when it belongs to the default workspace.
  • Updated factory-mode tests to cover personal plus organization workspaces and fail-fast workspace discovery errors.

Implementation Details

The factory path now mirrors the already-correct multi-workspace discovery path instead of doing a one-off ProjectClient.list_projects() call and best-effort workspace metadata lookup. Workspace/project discovery failures now surface to the caller instead of silently returning an incomplete list.

Testing

  • uv run pytest tests/mcp/test_tool_project_management.py -q -> 17 passed
  • uv run ruff check src/basic_memory/mcp/tools/project_management.py tests/mcp/test_tool_project_management.py -> passed
  • uv run ruff format --check src/basic_memory/mcp/tools/project_management.py tests/mcp/test_tool_project_management.py -> passed
  • just fast-check -> lint, format, and typecheck passed; testmon ran 2794 tests successfully but failed 14 Docker/testcontainers-backed semantic fixtures because the local Docker Unix socket was unavailable (FileNotFoundError while probing Docker /version).

Risks / Follow-ups

The cloud repo still needs to pin basic-memory to this commit so issue basicmachines-co/basic-memory-cloud#684 is fixed in web-v2/cloud deployments.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 2, 2026

Claude finished @phernandez's task in 1m 44s —— View job


PR Review: List factory MCP projects across workspaces

  • Gather context and read changed files
  • Review code quality & standards
  • Review testing & documentation
  • Review architecture & patterns
  • Post final review summary

Summary

This is a clean, well-targeted fix. The factory mode path was doing a one-off ProjectClient.list_projects() call (which only returns projects in the default workspace) and then attempting a best-effort workspace metadata lookup with a swallowed except Exception: pass. The fix correctly mirrors what the non-factory path already does: use ensure_workspace_project_index() to fan out across all accessible workspaces at once.


Code Quality & Standards Checklist

Item Status
Follows Basic Memory coding conventions
Python 3.12+ type annotations and async patterns
SQLAlchemy 2.0 best practices ✅ (N/A for changed code)
100-character line length limit
No commented-out code blocks
Unit tests for changed logic
Integration test coverage
Edge cases tested
100% coverage maintained
Windows-compatible file path handling
No emojis/unicode in new console output

What Changed

src/basic_memory/mcp/tools/project_management.py (+10/-35)

The factory-mode block in list_memory_projects (line 288) is replaced:

Before: Called ProjectClient.list_projects() once → only default workspace → then tried to look up workspace metadata in a try/except Exception: pass block (silently hiding failures and returning an incomplete list)

After:

if is_factory_mode():
    workspace_index = await ensure_workspace_project_index(context=context)
    merged = _merge_workspace_projects(None, workspace_index.entries)
    default_project = next(
        (entry.project.name for entry in workspace_index.entries
         if entry.workspace.is_default and entry.project.is_default),
        None,
    )

This is the correct approach — reusing the already-correct multi-workspace index path rather than duplicating a broken subset of it.

Fail-fast is the right call here. Factory mode runs in a cloud app context where there's no local fallback and no partial list is better than a complete one. Surfacing discovery errors to the caller (rather than silently returning an empty/incomplete list) matches the existing behavior of the non-factory path and gives callers actionable signal.

default_project detection (lines 291–297) is correct: it finds the project marked is_default in the is_default workspace. Edge case where neither a default workspace nor a default project exists gracefully returns None via next(..., None).


Testing

tests/mcp/test_tool_project_management.py (+74/-42)

Three factory-mode tests are updated/added:

  1. test_list_memory_projects_factory_mode — now covers a personal workspace + org workspace scenario, verifies the workspace header appears in text output, verifies external_id appears in entries, and adds mock_index.assert_awaited_once() — a nice check that the index isn't called redundantly.

  2. test_list_memory_projects_factory_mode_json_includes_workspace — now verifies result["default_project"] == "personal-main" (personal workspace default project), both projects appear, and all workspace fields are correctly propagated.

  3. test_list_memory_projects_factory_mode_workspace_lookup_failure — correctly inverted: previously asserted graceful fallback; now correctly asserts pytest.raises(RuntimeError), matching the new fail-fast behavior.


Minor Observations (Non-blocking)

1. Unused-in-factory-path ProjectClient import

In list_memory_projects, line 282:

from basic_memory.mcp.clients import ProjectClient

This deferred import runs before the if is_factory_mode(): check. In factory mode the function returns early, so ProjectClient is imported but unused in that path. Python caches module imports so cost is negligible, and this is pre-existing style (not introduced by this PR). Worth noting but not worth changing now.

2. Pre-existing dead code in the non-factory else branch

At line 338, when cloud_entries is empty:

merged = _merge_projects(local_list, cloud_list, cloud_workspace_name=cloud_ws_name, ...)

cloud_list is always None here (it's initialized to None and never assigned), so this branch is effectively _merge_projects(local_list, None) — a local-only merge. This is correct behavior (local-only fallback when cloud discovery fails), but the extra keyword args being passed are all None. Pre-existing from before this PR and not in scope to clean up here.


Verdict

LGTM. The change is minimal, correct, and well-tested. The fail-fast behavior in factory mode is the right design choice. No blocking issues.

Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez phernandez force-pushed the codex-684-list-projects-workspaces branch from 4c9fc28 to 014cbd3 Compare May 2, 2026 01:42
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 2, 2026

Claude finished @phernandez's task in 1m 52s —— View job


PR Review: List factory MCP projects across workspaces

  • Gather context and read changed files
  • Review code quality & standards
  • Review testing & documentation
  • Review architecture & patterns
  • Post final review summary

Summary

This PR fixes a real bug: in factory/cloud mode, list_memory_projects was calling ProjectClient.list_projects() on the default workspace only, hiding all team/org workspace projects from callers. The fix replaces that one-off call with ensure_workspace_project_index, which is already used by the normal stdio path for multi-workspace discovery. Good consistency improvement.


Code Quality & Standards

Item Status
Follows Basic Memory conventions in CLAUDE.md
Python 3.12+ type annotations and async patterns
Literate programming section headers / decision comments
100-character line length
No commented-out code
Fail-fast (no fallback swallowed)

The comment header on the factory block was correctly updated to reflect the new intent. The pass # workspace lookup is best-effort pattern (the old anti-pattern) is gone — good.


Architecture & Correctness

Factory path now (correct):

workspace_index = await ensure_workspace_project_index(context=context)
merged = _merge_workspace_projects(None, workspace_index.entries)

Factory path before (incorrect):

async with get_client() as client:
    project_list = await ProjectClient(client).list_projects()
# then a best-effort workspace lookup that could silently miss org workspaces

The new path calls the same ensure_workspace_project_index function used by the non-factory has_cloud_credentials branch, so both paths go through identical workspace discovery logic. This symmetry is the right design.

default_project selection logic (project_management.py:291–298) is correct: it finds the first project where workspace.is_default AND project.is_default. This is semantically tighter than the old project_list.default_project which came from a single workspace fetch.

WorkspaceProjectIndex.failed_workspaces — a minor pre-existing behaviour worth noting: if a subset of workspaces fail during index building, failed_workspaces is populated but ensure_workspace_project_index still returns (doesn't raise). Factory mode won't surface those partial failures. This is not introduced by this PR and is acceptable for now, but could be an explicit follow-up.


Testing & Documentation

Item Status
Personal + org workspace coverage in text output test
JSON output test validates default_project, workspace metadata
Fail-fast error propagation test (RuntimeError raised)
Previous graceful-degradation expectation correctly flipped
100% coverage maintained

The test helpers _make_workspace / _make_workspace_index are reused from the existing test suite; the new tests slot in cleanly.

One gap: no test for factory mode + constrained_project env var set (i.e., the _format_constrained_text branch in factory mode, project_management.py:301–302). This path existed before the PR and isn't regression territory here, but it's technically reachable.


Security & Compatibility

Item Status
No hardcoded secrets
No Unicode/emoji in console output
Windows-safe (no path comparisons introduced)
No sensitive data in logs

Verdict

Approve. The change is minimal, well-scoped, and correct. The factory path now mirrors the already-tested multi-workspace discovery path instead of reinventing a one-workspace subset. Tests are solid. The behavior change in error handling (fail-fast vs. silent degradation) is intentional and appropriate for factory mode.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c9fc28d9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +289 to +290
workspace_index = await ensure_workspace_project_index(context=context)
merged = _merge_workspace_projects(None, workspace_index.entries)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface partial workspace discovery failures

Handle the failed_workspaces case before formatting results: ensure_workspace_project_index() can return a partial index when one or more workspace project fetches fail, and this factory-mode path currently renders workspace_index.entries as if discovery were complete. In that scenario, list_memory_projects silently omits projects from failed workspaces, so callers can miss external_ids and make incorrect routing decisions even though the tool appears successful.

Useful? React with 👍 / 👎.

@phernandez phernandez changed the title List factory MCP projects across workspaces fix(mcp): list factory projects across workspaces May 2, 2026
@phernandez phernandez merged commit b4bf14e into main May 2, 2026
42 of 44 checks passed
@phernandez phernandez deleted the codex-684-list-projects-workspaces branch May 2, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant