Skip to content

Commit 0b33547

Browse files
phernandezclaude
andauthored
fix(mcp): resolve projects by external_id, remove workspace from MCP tools (#777)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2fccc74 commit 0b33547

31 files changed

Lines changed: 688 additions & 447 deletions

docs/ai-assistant-guide-extended.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,19 @@ SQLite Database (Index)
9191
# List all projects
9292
projects = await list_memory_projects()
9393

94-
# Response structure:
94+
# Response structure (each entry includes external_id you can pass as project_id):
9595
# [
9696
# {
9797
# "name": "main",
98+
# "external_id": "550e8400-e29b-41d4-a716-446655440000",
9899
# "path": "/Users/name/notes",
99100
# "is_default": True,
100101
# "note_count": 156,
101102
# "last_synced": "2025-01-15T10:30:00Z"
102103
# },
103104
# {
104105
# "name": "work",
106+
# "external_id": "9f86d081-884c-42a3-b5e3-1c0c5b4c8e52",
105107
# "path": "/Users/name/work-notes",
106108
# "is_default": False,
107109
# "note_count": 89,
@@ -164,6 +166,44 @@ active_project = "main"
164166
results = await search_notes(query="topic", project=active_project)
165167
```
166168

169+
### `project` vs `project_id`
170+
171+
Every project has two identifiers:
172+
173+
- **`project`** — human-readable name (e.g., `"main"`). Easy to use, but can collide across cloud workspaces.
174+
- **`project_id`** — stable `external_id` UUID. Always unambiguous; takes precedence over `project` when both are passed.
175+
176+
**When to prefer `project_id`:**
177+
178+
1. **Cloud multi-workspace setups.** If the user belongs to more than one workspace (personal + organization, or several organizations) and the same project name might exist in more than one of them, pass `project_id` to route to the exact project. Without it, name resolution falls back to the default workspace, which may not be the one the user means.
179+
2. **After `list_memory_projects()`.** Once you have the `external_id`, prefer using it — it's the same number of characters in JSON and saves a name-resolution round-trip.
180+
3. **When persisting a project choice across a long session.** UUIDs are stable; names can be renamed.
181+
182+
**When `project` (name) is fine:**
183+
184+
- Local single-workspace setups (no collision risk).
185+
- One-off operations where the name is clearly visible to the user (e.g., quick `search_notes(project="main", ...)`).
186+
- The user explicitly references a project by name in their message.
187+
188+
**Example — cloud multi-workspace pattern:**
189+
190+
```python
191+
# Discover and pick the right project for this user
192+
projects = await list_memory_projects()
193+
target = next(p for p in projects if p["name"] == "research" and p["workspace"]["slug"] == "acme")
194+
195+
# Use the UUID for all subsequent operations — no ambiguity
196+
await write_note(
197+
title="Meeting Notes",
198+
content="...",
199+
folder="meetings",
200+
project_id=target["external_id"],
201+
)
202+
results = await search_notes(query="kickoff", project_id=target["external_id"])
203+
```
204+
205+
**Precedence rule:** When both are passed, `project_id` wins. This lets you safely supply `project="main"` for backward compatibility while still routing precisely with `project_id`.
206+
167207
### Cross-Project Operations
168208

169209
**Some tools work across all projects when project parameter omitted:**

src/basic_memory/cli/commands/db.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ def _abort_if_mcp_processes_alive() -> None:
9999
if not zombies:
100100
return
101101

102-
console.print(
103-
"[red]Refusing to reset:[/red] basic-memory MCP processes are still running."
104-
)
102+
console.print("[red]Refusing to reset:[/red] basic-memory MCP processes are still running.")
105103
console.print(
106104
"[yellow]On macOS/Linux these would keep reading the deleted memory.db inode "
107105
"and return phantom search results (see #765).[/yellow]"
@@ -117,9 +115,7 @@ def _abort_if_mcp_processes_alive() -> None:
117115
"Where-Object {$_.CommandLine -like '*basic-memory*mcp*'}[/green]"
118116
)
119117
else:
120-
console.print(
121-
" 2. Verify nothing remains: [green]pgrep -fa 'basic-memory mcp'[/green]"
122-
)
118+
console.print(" 2. Verify nothing remains: [green]pgrep -fa 'basic-memory mcp'[/green]")
123119
console.print(" 3. Re-run [green]bm reset[/green].")
124120
raise typer.Exit(1)
125121

src/basic_memory/cli/commands/tool.py

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,12 @@ def write_note(
6262
help="The project to write to. If not provided, the default project will be used."
6363
),
6464
] = None,
65-
workspace: Annotated[
65+
project_id: Annotated[
6666
Optional[str],
67-
typer.Option(help="Cloud workspace tenant ID or unique name to route this request."),
67+
typer.Option(
68+
"--project-id",
69+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
70+
),
6871
] = None,
6972
local: bool = typer.Option(
7073
False, "--local", help="Force local API routing (ignore cloud mode)"
@@ -106,7 +109,7 @@ def write_note(
106109
content=content,
107110
directory=folder,
108111
project=project,
109-
workspace=workspace,
112+
project_id=project_id,
110113
tags=tags,
111114
output_format="json",
112115
)
@@ -132,9 +135,12 @@ def read_note(
132135
Optional[str],
133136
typer.Option(help="The project to use. If not provided, the default project will be used."),
134137
] = None,
135-
workspace: Annotated[
138+
project_id: Annotated[
136139
Optional[str],
137-
typer.Option(help="Cloud workspace tenant ID or unique name to route this request."),
140+
typer.Option(
141+
"--project-id",
142+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
143+
),
138144
] = None,
139145
local: bool = typer.Option(
140146
False, "--local", help="Force local API routing (ignore cloud mode)"
@@ -156,7 +162,7 @@ def read_note(
156162
mcp_read_note(
157163
identifier=identifier,
158164
project=project,
159-
workspace=workspace,
165+
project_id=project_id,
160166
include_frontmatter=include_frontmatter,
161167
output_format="json",
162168
)
@@ -195,9 +201,12 @@ def edit_note(
195201
help="The project to edit. If not provided, the default project will be used."
196202
),
197203
] = None,
198-
workspace: Annotated[
204+
project_id: Annotated[
199205
Optional[str],
200-
typer.Option(help="Cloud workspace tenant ID or unique name to route this request."),
206+
typer.Option(
207+
"--project-id",
208+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
209+
),
201210
] = None,
202211
local: bool = typer.Option(
203212
False, "--local", help="Force local API routing (ignore cloud mode)"
@@ -222,7 +231,7 @@ def edit_note(
222231
operation=operation,
223232
content=content,
224233
project=project,
225-
workspace=workspace,
234+
project_id=project_id,
226235
section=section,
227236
find_text=find_text,
228237
expected_replacements=expected_replacements,
@@ -260,9 +269,12 @@ def build_context(
260269
Optional[str],
261270
typer.Option(help="The project to use. If not provided, the default project will be used."),
262271
] = None,
263-
workspace: Annotated[
272+
project_id: Annotated[
264273
Optional[str],
265-
typer.Option(help="Cloud workspace tenant ID or unique name to route this request."),
274+
typer.Option(
275+
"--project-id",
276+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
277+
),
266278
] = None,
267279
local: bool = typer.Option(
268280
False, "--local", help="Force local API routing (ignore cloud mode)"
@@ -284,7 +296,7 @@ def build_context(
284296
mcp_build_context(
285297
url=url,
286298
project=project,
287-
workspace=workspace,
299+
project_id=project_id,
288300
depth=depth,
289301
timeframe=timeframe,
290302
page=page,
@@ -317,9 +329,12 @@ def recent_activity(
317329
Optional[str],
318330
typer.Option(help="The project to use. If not provided, the default project will be used."),
319331
] = None,
320-
workspace: Annotated[
332+
project_id: Annotated[
321333
Optional[str],
322-
typer.Option(help="Cloud workspace tenant ID or unique name to route this request."),
334+
typer.Option(
335+
"--project-id",
336+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
337+
),
323338
] = None,
324339
local: bool = typer.Option(
325340
False, "--local", help="Force local API routing (ignore cloud mode)"
@@ -346,7 +361,7 @@ def recent_activity(
346361
page=page,
347362
page_size=page_size,
348363
project=project,
349-
workspace=workspace,
364+
project_id=project_id,
350365
output_format="json",
351366
)
352367
)
@@ -408,9 +423,12 @@ def search_notes(
408423
Optional[str],
409424
typer.Option(help="The project to use. If not provided, the default project will be used."),
410425
] = None,
411-
workspace: Annotated[
426+
project_id: Annotated[
412427
Optional[str],
413-
typer.Option(help="Cloud workspace tenant ID or unique name to route this request."),
428+
typer.Option(
429+
"--project-id",
430+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
431+
),
414432
] = None,
415433
local: bool = typer.Option(
416434
False, "--local", help="Force local API routing (ignore cloud mode)"
@@ -482,7 +500,7 @@ def search_notes(
482500
mcp_search(
483501
query=query or None,
484502
project=project,
485-
workspace=workspace,
503+
project_id=project_id,
486504
search_type=search_type,
487505
output_format="json",
488506
page=page,
@@ -592,9 +610,12 @@ def schema_validate(
592610
Optional[str],
593611
typer.Option(help="The project to use. If not provided, the default project will be used."),
594612
] = None,
595-
workspace: Annotated[
613+
project_id: Annotated[
596614
Optional[str],
597-
typer.Option(help="Cloud workspace tenant ID or unique name to route this request."),
615+
typer.Option(
616+
"--project-id",
617+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
618+
),
598619
] = None,
599620
local: bool = typer.Option(
600621
False, "--local", help="Force local API routing (ignore cloud mode)"
@@ -629,7 +650,7 @@ def schema_validate(
629650
note_type=note_type,
630651
identifier=identifier,
631652
project=project,
632-
workspace=workspace,
653+
project_id=project_id,
633654
output_format="json",
634655
)
635656
)
@@ -660,9 +681,12 @@ def schema_infer(
660681
Optional[str],
661682
typer.Option(help="The project to use. If not provided, the default project will be used."),
662683
] = None,
663-
workspace: Annotated[
684+
project_id: Annotated[
664685
Optional[str],
665-
typer.Option(help="Cloud workspace tenant ID or unique name to route this request."),
686+
typer.Option(
687+
"--project-id",
688+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
689+
),
666690
] = None,
667691
local: bool = typer.Option(
668692
False, "--local", help="Force local API routing (ignore cloud mode)"
@@ -686,7 +710,7 @@ def schema_infer(
686710
note_type=note_type,
687711
threshold=threshold,
688712
project=project,
689-
workspace=workspace,
713+
project_id=project_id,
690714
output_format="json",
691715
)
692716
)
@@ -714,9 +738,12 @@ def schema_diff(
714738
Optional[str],
715739
typer.Option(help="The project to use. If not provided, the default project will be used."),
716740
] = None,
717-
workspace: Annotated[
741+
project_id: Annotated[
718742
Optional[str],
719-
typer.Option(help="Cloud workspace tenant ID or unique name to route this request."),
743+
typer.Option(
744+
"--project-id",
745+
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
746+
),
720747
] = None,
721748
local: bool = typer.Option(
722749
False, "--local", help="Force local API routing (ignore cloud mode)"
@@ -738,7 +765,7 @@ def schema_diff(
738765
mcp_schema_diff(
739766
note_type=note_type,
740767
project=project,
741-
workspace=workspace,
768+
project_id=project_id,
742769
output_format="json",
743770
)
744771
)

0 commit comments

Comments
 (0)