Skip to content

MCP writes should return workspace-qualified canonical permalinks for routed cloud projects #847

Description

@groksrc

Bug Description

When an MCP write is routed to a cloud project by project_id or by workspace-qualified project, the tool output may return a project-qualified permalink such as:

getting-started/hermes-tests/hermes-bm-smoke-20260524-t142716-z-personal-getting-started

instead of a workspace-qualified canonical permalink such as:

personal/getting-started/hermes-tests/hermes-bm-smoke-20260524-t142716-z-personal-getting-started

In a mixed local+cloud / multi-workspace setup, that returned permalink is not reliably self-routing. Follow-up reads without explicit project / project_id can be routed through the Hermes/Basic Memory configured default project and fail, even though the note was written successfully.

This is especially confusing for agents because the natural workflow is:

  1. write_note(project_id=...)
  2. parse returned permalink
  3. use returned permalink for read_note / edit_note / delete_note

That workflow is only safe if the returned identifier is canonical enough to self-route.

Environment

Observed through Hermes bm_* MCP tools after the Basic Memory / hermes-basic-memory routing updates.

  • Basic Memory runtime: Basic Memory version: 0.21.3
  • Hermes plugin: basic-memory / hermes-basic-memory 0.3.2
  • Hermes provider default project: hermes-memory
  • Workspaces discovered:
    • personal / Personal
    • basic-memory-7020de4e925843c68c9056c60d101d9e / Basic Memory org

Steps to Reproduce

Use a setup with two cloud workspaces that both include a project with the same name, e.g. getting-started in both Personal and org workspaces.

  1. Discover projects:
bm_projects()

Relevant entries observed:

{
  "name": "getting-started",
  "external_id": "9e1f395d-16c1-4398-988a-df34d46d8bba",
  "workspace_slug": "personal",
  "qualified_name": "personal/getting-started"
}
{
  "name": "getting-started",
  "external_id": "9d432f92-3a9b-4cc8-aecc-698951490ef8",
  "workspace_slug": "basic-memory-7020de4e925843c68c9056c60d101d9e",
  "qualified_name": "basic-memory-7020de4e925843c68c9056c60d101d9e/getting-started"
}
  1. Write to Personal getting-started by project_id:
bm_write(
    project_id="9e1f395d-16c1-4398-988a-df34d46d8bba",
    folder="hermes-tests",
    title="Hermes BM Smoke 20260524T142716Z personal getting-started",
    content="..."
)
  1. Observe returned output:
# Created note
project: getting-started
file_path: hermes-tests/Hermes BM Smoke 20260524T142716Z personal getting-started.md
permalink: getting-started/hermes-tests/hermes-bm-smoke-20260524-t142716-z-personal-getting-started
  1. Try to read using the returned permalink without explicit routing:
bm_read(identifier="getting-started/hermes-tests/hermes-bm-smoke-20260524-t142716-z-personal-getting-started")

Observed result from Hermes with default project hermes-memory:

# Note Not Found in hermes-memory: "getting-started/hermes-tests/hermes-bm-smoke-20260524-t142716-z-personal-getting-started"
  1. Reading with a workspace-qualified identifier works:
bm_read(identifier="memory://personal/getting-started/hermes-tests/hermes-bm-smoke-20260524-t142716-z-personal-getting-started")

The same pattern was observed for the org getting-started project and other cloud-only projects where the returned permalink did not include the workspace slug.

Expected Behavior

For routed cloud writes, Basic Memory should return a canonical identifier that is safe for follow-up calls. Preferably:

permalink: personal/getting-started/hermes-tests/...

or, for the org workspace:

permalink: basic-memory-7020de4e925843c68c9056c60d101d9e/getting-started/hermes-tests/...

At minimum, MCP write/read/search results should include an explicit qualified_name / workspace_slug / canonical memory URL in structured output so clients and agents can safely route follow-up operations.

Actual Behavior

The write succeeds, but the returned permalink may be only project-qualified:

getting-started/hermes-tests/...

That identifier is ambiguous across workspaces and is not safe to use as-is from a Hermes session whose default project is different.

Why this matters

This breaks the agent/client invariant that “the permalink returned by a successful write is the identifier to use for follow-up read/edit/delete.” Agents either have to carry project_id manually forever or reconstruct workspace-qualified identifiers themselves from bm_projects() output.

Suggested Fix / Regression Tests

Suggested behavior:

  • If a call was routed by project_id, return a workspace-qualified canonical permalink when the project belongs to a cloud workspace.
  • If a call was routed by workspace-qualified project, return a workspace-qualified canonical permalink.
  • Preserve existing local-only behavior where no workspace exists.
  • Ensure duplicate project names across workspaces return distinct canonical permalinks.

Suggested tests:

  1. write_note(project_id=<personal getting-started uuid>) returns personal/getting-started/... or a canonical memory://personal/getting-started/... URL.
  2. write_note(project_id=<org getting-started uuid>) returns basic-memory-.../getting-started/....
  3. The returned identifier can be passed directly to read_note without specifying project / project_id.
  4. Existing workspace-qualified memory://... read/context routing continues to work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcloudBasic Memory Cloud

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions