feat(mcp): discover projects across workspaces#757
Conversation
Signed-off-by: phernandez <paul@basicmachines.co>
4882545 to
49148fe
Compare
|
Claude finished @phernandez's task in 2m 36s —— View job PR Review: feat(mcp): discover projects across workspacesTasks
SummaryGood implementation of cross-workspace project discovery. The caching strategy, workspace/project index, and routing fallback chain are well-designed. Test coverage is thorough and the new scenarios are clearly expressed. A few issues worth addressing before merge: Code Quality & Standards✅ Passing
|
| Item | Status |
|---|---|
| Follows coding conventions | ✅ |
| Python 3.12+ type annotations and async patterns | ✅ |
| FastAPI/Typer conventions followed | ✅ |
| 100-character line length maintained | ✅ |
| No commented-out code blocks | ✅ |
| Unit tests for new functions | ✅ |
| Test coverage for edge cases | ✅ |
| MCP tools follow atomic, composable design | ✅ |
| Preserves local-first architecture | ✅ |
| No hardcoded secrets or credentials | ✅ |
| No sensitive data in logs | ✅ |
| Markdown file handling preserved | ✅ |
Factory mode workspace_slug missing in listing |
❌ Bug |
ProjectItem.is_default mutation |
|
Importing private _ensure_workspace_project_index |
|
Unicode • in console output (new) |
|
Duplicate _workspace() test helper |
|
| Breaking schema change (intentional, needs cloud#619 first) | ℹ️ Note |
The factory-mode slug omission (#1) is the only actionable bug. The rest are minor code-quality items.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 48825456d0
ℹ️ 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".
| fetched_entries = await asyncio.gather( | ||
| *[_fetch_workspace_project_entries(workspace, context=context) for workspace in workspaces] | ||
| ) |
There was a problem hiding this comment.
Isolate per-workspace fetch failures when indexing projects
_ensure_workspace_project_index builds the cross-workspace index with asyncio.gather(...) in fail-fast mode, so one list_projects error from any workspace aborts the entire index. In multi-workspace accounts this makes resolve_workspace_project_identifier/get_project_client fail globally (including qualified lookups for healthy workspaces) whenever a single workspace is temporarily failing or unauthorized, which is a regression from the previous single-workspace routing behavior.
Useful? React with 👍 / 👎.
| resolved_entry = await resolve_workspace_project_identifier( | ||
| resolved_project, | ||
| context=context, |
There was a problem hiding this comment.
Preserve factory-only routing for project resolution
This factory/cloud path now resolves projects through resolve_workspace_project_identifier, which depends on workspace discovery before opening the injected client. In deployments that only configure set_client_factory() (without also registering a workspace provider), this can fall back to control-plane auth and fail before the factory transport is used, breaking previously valid factory integrations that relied on in-process tenant routing.
Useful? React with 👍 / 👎.
Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 2m 12s —— View job PR Review: feat(mcp): discover projects across workspacesTasks
SummaryThis PR correctly addresses all three actionable bugs from the previous review round. The caching strategy, workspace/project index, and routing fallback chain are well-designed. Two minor items and one open question remain. Previous Review — Status
Remaining Items
|
| Item | Status |
|---|---|
| Follows coding conventions | ✅ |
| Python 3.12+ type annotations and async patterns | ✅ |
| FastAPI/Typer conventions followed | ✅ |
| 100-character line length maintained | ✅ |
| No commented-out code blocks | ✅ |
| Unit tests for new functions | ✅ |
| Test coverage for edge cases | ✅ |
| MCP tools follow atomic, composable design | ✅ |
| Preserves local-first architecture | ✅ |
| No hardcoded secrets or credentials | ✅ |
| No sensitive data in logs | ✅ |
| File path comparisons Windows-compatible | ✅ |
Unicode • in console output (new, line 253) |
|
Duplicate _workspace() / _ContextState test helpers |
|
asyncio.gather fail-fast in multi-workspace index build |
ℹ️ Trade-off to decide |
| Factory routing implicit workspace provider contract | ℹ️ Integration note |
Breaking schema change (WorkspaceInfo.slug/is_default) |
ℹ️ Intentional per cloud#619 |
Signed-off-by: phernandez <paul@basicmachines.co>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6b0f19ca81
ℹ️ 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".
| elif project_entry and project_entry.workspace_id: | ||
| active_ws = await resolve_workspace_parameter( | ||
| workspace=project_entry.workspace_id, | ||
| context=context, |
There was a problem hiding this comment.
Skip workspace discovery when tenant id is already configured
This branch re-resolves a known workspace_id through resolve_workspace_parameter, which forces a /workspaces discovery call before opening the project client. That creates a new hard dependency on control-plane workspace listing for every cloud project that already has ProjectEntry.workspace_id set: if workspace discovery is temporarily failing or unauthorized, project routing now fails even though the tenant ID is already known and could be sent directly to get_client (as before).
Useful? React with 👍 / 👎.
| active_workspace = await resolve_workspace_parameter(workspace, context) | ||
| cloud_list = await _fetch_cloud_projects(active_workspace.tenant_id, context) |
There was a problem hiding this comment.
Preserve local fallback for explicit workspace project listing
The explicit-workspace path now calls resolve_workspace_parameter before _fetch_cloud_projects. Because _fetch_cloud_projects is where cloud-list failures are intentionally downgraded to local-only results, any failure during workspace discovery (control-plane outage, auth mismatch, etc.) now aborts list_memory_projects outright instead of returning the local project list as the tool’s fallback behavior promises.
Useful? React with 👍 / 👎.
|
Claude finished @phernandez's task in 1m 40s —— View job PR Review: feat(mcp): discover projects across workspacesTasks
SummaryThe third commit ( Previous Review Items — Final Status
asyncio.gather Fix — Quality Check ✅The
The Remaining Minor ItemDuplicate Three files define functionally identical helpers:
Checklist
|
Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 1m 43s —— View job PR Review: feat(mcp): discover projects across workspacesTasks
SummaryThe fourth commit ( Previous Review Items — Final Status
Latest Commit Quality Check ✅
The
The The new test Remaining Minor ItemDuplicate Three files define functionally identical helpers:
Checklist
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38aeedfdd8
ℹ️ 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".
| if context: | ||
| cached_raw = await context.get_state(_WORKSPACE_PROJECT_INDEX_STATE_KEY) | ||
| cached_index = _workspace_project_index_from_state(cached_raw) | ||
| if cached_index is not None: | ||
| return cached_index |
There was a problem hiding this comment.
Rebuild partial workspace index after transient failures
_ensure_workspace_project_index returns any cached index immediately, even when that cached index was built with failed_workspaces. If one workspace project fetch fails once (for example a transient outage or temporary auth issue), that partial state is reused for the rest of the MCP session and keeps producing “discovery failed” routing errors after the workspace recovers, until the caller manually invalidates state or starts a new session.
Useful? React with 👍 / 👎.
| if active_ws is not None: | ||
| await _set_cached_active_workspace(context, active_ws) |
There was a problem hiding this comment.
Clear stale workspace cache on workspace_id routing
The active workspace cache is only updated when active_ws is populated, but the project_entry.workspace_id path never sets active_ws; it only sets workspace_id. In multi-workspace sessions, this leaves the previous workspace cached and later unqualified duplicate project names can be resolved against the wrong tenant via the cached-workspace shortcut in resolve_workspace_project_identifier.
Useful? React with 👍 / 👎.
Summary
slug,is_default,default_workspace_id) and use them for workspace-qualified project routingworkspace-slug/projectand report ambiguous unqualified nameslist_memory_projectsacross accessible workspaces while preserving local-only fallback for listing UXlist_workspacesfriendly for non-teams/local-only users with a display-only personal workspace fallbackValidation
uv run pytest tests/mcp/test_tool_workspace_management.py tests/mcp/test_tool_project_management.py tests/mcp/test_project_context.py tests/mcp/test_project_context_telemetry.py tests/cli/test_workspace_commands.py tests/cli/test_project_info_cloud_status.py tests/cli/test_project_add_with_local_path.py tests/cli/test_project_set_cloud_local.py -quv run ruff check src/basic_memory/mcp/project_context.py src/basic_memory/mcp/tools/project_management.py src/basic_memory/mcp/tools/utils.py src/basic_memory/mcp/tools/workspaces.py src/basic_memory/schemas/cloud.py tests/cli/test_project_add_with_local_path.py tests/cli/test_project_info_cloud_status.py tests/cli/test_project_set_cloud_local.py tests/cli/test_workspace_commands.py tests/mcp/test_project_context.py tests/mcp/test_project_context_telemetry.py tests/mcp/test_tool_project_management.py tests/mcp/test_tool_workspace_management.pyuv run ty check src/basic_memory/mcp/project_context.py src/basic_memory/mcp/tools/project_management.py src/basic_memory/mcp/tools/utils.py src/basic_memory/mcp/tools/workspaces.py src/basic_memory/schemas/cloud.py tests/cli/test_project_add_with_local_path.py tests/cli/test_project_info_cloud_status.py tests/cli/test_project_set_cloud_local.py tests/cli/test_workspace_commands.py tests/mcp/test_project_context.py tests/mcp/test_project_context_telemetry.py tests/mcp/test_tool_project_management.py tests/mcp/test_tool_workspace_management.pygit diff --checkCloses #756
Depends on basicmachines-co/basic-memory-cloud#619