Skip to content

Remove X-Workspace-ID requirement from MCP — aggregate projects across workspaces #756

Description

@phernandez

Background

When a user is on a teams plan, the MCP client currently must pass X-Workspace-ID on every cloud request (via an explicit workspace argument or config.default_workspace). That means list_memory_projects shows projects from only one workspace at a time, and users must know which workspace to target before calling any tool.

We want the MCP to feel workspace-free: users see the union of all projects they can access (personal + every org they're a member of), with personal/default workspace behavior expressed by cloud metadata and disambiguation only when project names actually collide across workspaces.

The X-Workspace-ID header stays on the wire — this is purely about removing it from the user-visible API. The header will be auto-injected by the client after resolving a project to a workspace.

Design (client-side resolution)

  1. On first project-scoped cloud lookup, fetch workspace discovery: GET /workspaces/.
  2. Use explicit cloud response fields (slug, is_default, default_workspace_id) instead of relying on list position for default semantics.
  3. Fan out project discovery for each accessible workspace: GET /projects/ with X-Workspace-ID for that workspace.
  4. Build a per-session index: {project_permalink -> [(workspace_tenant_id, workspace_slug, project)]}.
  5. On project-scoped tool calls: resolve project -> inject the correct X-Workspace-ID.
  6. Collisions fail with actionable qualified choices (Use: personal/meeting-notes or acme/meeting-notes).
  7. Qualified identifiers (<slug>/<project>) route directly to the matching workspace.

Backend transport (tenant_asgi.py, WorkspaceResolver, subscription gating) is unchanged.

User-visible behavior

Non-teams user: identical to today. One workspace, no collisions, no workspace vocabulary.

Teams user:

  • list_memory_projects returns the union, grouped by workspace, default/personal first.
  • read_note("sprint-plan") — unambiguous, routes automatically.
  • read_note("meeting-notes") where it exists in two workspaces — fails with Use: personal/meeting-notes or acme/meeting-notes.
  • read_note("acme/meeting-notes") — qualified, routes to acme.

Tasks

Core client

  • Update cloud schemas
    • Add slug to WorkspaceInfo
    • Add is_default to WorkspaceInfo
    • Add default_workspace_id to WorkspaceListResponse
    • Preserve compatibility with older cloud responses while cloud deploys roll out, if needed
  • Add workspace/project index cache to mcp/project_context.py (per-session context)
    • _ensure_workspace_index() — lazy fan-out on first use
    • Parallel GET /projects/ calls across workspaces
    • Store workspace slug, tenant id, display name, role, default flag, and project metadata
    • Invalidate on create_memory_project / delete_project
  • Add qualified-identifier resolver (<slug>/<project> -> (tenant_id, project_permalink))
  • Auto-inject X-Workspace-ID in get_project_client() based on resolved project
    • Stop relying on config.default_workspace as the normal header-selection mechanism
    • Keep explicit workspace/config paths as compatibility overrides while we migrate
  • Collision handling
    • Detect duplicate project permalinks across workspaces
    • Raise with suggested qualified forms
    • Include workspace slugs and display names in the error

Tool surface

  • list_memory_projects — aggregate across all workspaces, grouped by workspace, default/personal first
  • Keep workspace parameter as an explicit/deprecated override for scripts and power users
  • create_memory_project — default target = cloud default workspace; accept <slug>/<name> to target an org
  • switch_project — accept qualified form; current project now carries workspace context
  • list_workspaces becomes informational only (no longer required for normal project-scoped flows)

Config / cleanup

  • Mark default_workspace as deprecated for MCP routing once index-based routing is available
  • Remove resolve_workspace_parameter() from normal user-facing project resolution paths
  • Update docs: no more "set your default workspace" onboarding step for MCP users

Tests

  • Multi-workspace fan-out fixture
  • Default marker is used instead of relying on response order
  • Collision -> error with qualified suggestions
  • Qualified-identifier resolution (happy path + unknown slug + unknown project)
  • Personal-only user (no teams) — behavior unchanged
  • Cache invalidation on project CRUD
  • API-key mode unaffected (still single-tenant)
  • Compatibility behavior for older workspace responses, if we decide to support mixed deploy windows

Depends on

Resolved decisions

  • Keep workspace as an explicit override/deprecated escape hatch for scripted use.
  • Do not rely on workspace list position alone for default semantics.
  • Do not change the cloud transport; continue to inject X-Workspace-ID under the hood.

Open questions

  • Cache TTL: per-session only, or add explicit refresh behavior/tool?
  • Do we need the optional cloud bulk-project endpoint after measuring fan-out latency?

Metadata

Metadata

Assignees

No one assigned

    Labels

    cloudBasic Memory CloudenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions