Skip to content

Commit af78cf8

Browse files
phernandezclaude
andcommitted
refactor: convert project_management.py to context manager pattern
Converts 3 project management tool functions to use async with get_client() context manager: - Changed import from 'client' to 'get_client' list_memory_projects(): - Wrapped function body at line 43 in async with block - Indented lines 44-71 inside context manager - Lists all available projects with session guidance create_memory_project(): - Wrapped function body at line 95 in async with block - Indented lines 96-126 inside context manager - Creates new project with optional default setting delete_project(): - Wrapped function body at line 150 in async with block - Indented lines 151-200 inside context manager - Deletes project with validation and confirmation All 3 functions passed typecheck with no errors. Total tools converted: 13/16 (Phase 0.3) Updated SPEC-16 Phase 0.3 checklist to mark project_management.py as completed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 208df1e commit af78cf8

2 files changed

Lines changed: 102 additions & 99 deletions

File tree

specs/SPEC-16 MCP Cloud Service Consolidation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ Convert from `from async_client import client` to `async with get_client() as cl
457457
- [x] `tools/move_note.py` (34/34 tests passing, 90% coverage)
458458
- [x] `tools/search.py` (16/16 tests passing, 96% coverage)
459459
- [x] `tools/recent_activity.py` (4/4 tests passing, 82% coverage)
460-
- [ ] `tools/project_management.py`
460+
- [x] `tools/project_management.py` (3 functions: list_memory_projects, create_memory_project, delete_project - typecheck passed)
461461
- [x] `tools/edit_note.py` (17/17 tests passing)
462462
- [x] `tools/canvas.py` (5/5 tests passing)
463463
- [x] `tools/build_context.py` (6/6 tests passing)

src/basic_memory/mcp/tools/project_management.py

Lines changed: 101 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import os
88
from fastmcp import Context
99

10-
from basic_memory.mcp.async_client import client
10+
from basic_memory.mcp.async_client import get_client
1111
from basic_memory.mcp.server import mcp
1212
from basic_memory.mcp.tools.utils import call_get, call_post, call_delete
1313
from basic_memory.schemas.project_info import (
@@ -40,34 +40,35 @@ async def list_memory_projects(context: Context | None = None) -> str:
4040
Example:
4141
list_memory_projects()
4242
"""
43-
if context: # pragma: no cover
44-
await context.info("Listing all available projects")
43+
async with get_client() as client:
44+
if context: # pragma: no cover
45+
await context.info("Listing all available projects")
4546

46-
# Check if server is constrained to a specific project
47-
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
47+
# Check if server is constrained to a specific project
48+
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
4849

49-
# Get projects from API
50-
response = await call_get(client, "/projects/projects")
51-
project_list = ProjectList.model_validate(response.json())
50+
# Get projects from API
51+
response = await call_get(client, "/projects/projects")
52+
project_list = ProjectList.model_validate(response.json())
5253

53-
if constrained_project:
54-
result = f"Project: {constrained_project}\n\n"
55-
result += "Note: This MCP server is constrained to a single project.\n"
56-
result += "All operations will automatically use this project."
57-
else:
58-
# Show all projects with session guidance
59-
result = "Available projects:\n"
54+
if constrained_project:
55+
result = f"Project: {constrained_project}\n\n"
56+
result += "Note: This MCP server is constrained to a single project.\n"
57+
result += "All operations will automatically use this project."
58+
else:
59+
# Show all projects with session guidance
60+
result = "Available projects:\n"
6061

61-
for project in project_list.projects:
62-
result += f"• {project.name}\n"
62+
for project in project_list.projects:
63+
result += f"• {project.name}\n"
6364

64-
result += "\n" + "─" * 40 + "\n"
65-
result += "Next: Ask which project to use for this session.\n"
66-
result += "Example: 'Which project should I use for this task?'\n\n"
67-
result += "Session reminder: Track the selected project for all subsequent operations in this conversation.\n"
68-
result += "The user can say 'switch to [project]' to change projects."
65+
result += "\n" + "─" * 40 + "\n"
66+
result += "Next: Ask which project to use for this session.\n"
67+
result += "Example: 'Which project should I use for this task?'\n\n"
68+
result += "Session reminder: Track the selected project for all subsequent operations in this conversation.\n"
69+
result += "The user can say 'switch to [project]' to change projects."
6970

70-
return result
71+
return result
7172

7273

7374
@mcp.tool("create_memory_project")
@@ -91,37 +92,38 @@ async def create_memory_project(
9192
create_memory_project("my-research", "~/Documents/research")
9293
create_memory_project("work-notes", "/home/user/work", set_default=True)
9394
"""
94-
# Check if server is constrained to a specific project
95-
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
96-
if constrained_project:
97-
return f'# Error\n\nProject creation disabled - MCP server is constrained to project \'{constrained_project}\'.\nUse the CLI to create projects: `basic-memory project add "{project_name}" "{project_path}"`'
98-
99-
if context: # pragma: no cover
100-
await context.info(f"Creating project: {project_name} at {project_path}")
101-
102-
# Create the project request
103-
project_request = ProjectInfoRequest(
104-
name=project_name, path=project_path, set_default=set_default
105-
)
95+
async with get_client() as client:
96+
# Check if server is constrained to a specific project
97+
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
98+
if constrained_project:
99+
return f'# Error\n\nProject creation disabled - MCP server is constrained to project \'{constrained_project}\'.\nUse the CLI to create projects: `basic-memory project add "{project_name}" "{project_path}"`'
100+
101+
if context: # pragma: no cover
102+
await context.info(f"Creating project: {project_name} at {project_path}")
103+
104+
# Create the project request
105+
project_request = ProjectInfoRequest(
106+
name=project_name, path=project_path, set_default=set_default
107+
)
106108

107-
# Call API to create project
108-
response = await call_post(client, "/projects/projects", json=project_request.model_dump())
109-
status_response = ProjectStatusResponse.model_validate(response.json())
109+
# Call API to create project
110+
response = await call_post(client, "/projects/projects", json=project_request.model_dump())
111+
status_response = ProjectStatusResponse.model_validate(response.json())
110112

111-
result = f"✓ {status_response.message}\n\n"
113+
result = f"✓ {status_response.message}\n\n"
112114

113-
if status_response.new_project:
114-
result += "Project Details:\n"
115-
result += f"• Name: {status_response.new_project.name}\n"
116-
result += f"• Path: {status_response.new_project.path}\n"
115+
if status_response.new_project:
116+
result += "Project Details:\n"
117+
result += f"• Name: {status_response.new_project.name}\n"
118+
result += f"• Path: {status_response.new_project.path}\n"
117119

118-
if set_default:
119-
result += "• Set as default project\n"
120+
if set_default:
121+
result += "• Set as default project\n"
120122

121-
result += "\nProject is now available for use in tool calls.\n"
122-
result += f"Use '{project_name}' as the project parameter in MCP tool calls.\n"
123+
result += "\nProject is now available for use in tool calls.\n"
124+
result += f"Use '{project_name}' as the project parameter in MCP tool calls.\n"
123125

124-
return result
126+
return result
125127

126128

127129
@mcp.tool()
@@ -145,53 +147,54 @@ async def delete_project(project_name: str, context: Context | None = None) -> s
145147
This action cannot be undone. The project will need to be re-added
146148
to access its content through Basic Memory again.
147149
"""
148-
# Check if server is constrained to a specific project
149-
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
150-
if constrained_project:
151-
return f"# Error\n\nProject deletion disabled - MCP server is constrained to project '{constrained_project}'.\nUse the CLI to delete projects: `basic-memory project remove \"{project_name}\"`"
152-
153-
if context: # pragma: no cover
154-
await context.info(f"Deleting project: {project_name}")
155-
156-
# Get project info before deletion to validate it exists
157-
response = await call_get(client, "/projects/projects")
158-
project_list = ProjectList.model_validate(response.json())
159-
160-
# Find the project by name (case-insensitive) or permalink - same logic as switch_project
161-
project_permalink = generate_permalink(project_name)
162-
target_project = None
163-
for p in project_list.projects:
164-
# Match by permalink (handles case-insensitive input)
165-
if p.permalink == project_permalink:
166-
target_project = p
167-
break
168-
# Also match by name comparison (case-insensitive)
169-
if p.name.lower() == project_name.lower():
170-
target_project = p
171-
break
172-
173-
if not target_project:
174-
available_projects = [p.name for p in project_list.projects]
175-
raise ValueError(
176-
f"Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
177-
)
178-
179-
# Call API to delete project using URL encoding for special characters
180-
from urllib.parse import quote
181-
182-
encoded_name = quote(target_project.name, safe="")
183-
response = await call_delete(client, f"/projects/{encoded_name}")
184-
status_response = ProjectStatusResponse.model_validate(response.json())
185-
186-
result = f"✓ {status_response.message}\n\n"
187-
188-
if status_response.old_project:
189-
result += "Removed project details:\n"
190-
result += f"• Name: {status_response.old_project.name}\n"
191-
if hasattr(status_response.old_project, "path"):
192-
result += f"• Path: {status_response.old_project.path}\n"
193-
194-
result += "Files remain on disk but project is no longer tracked by Basic Memory.\n"
195-
result += "Re-add the project to access its content again.\n"
196-
197-
return result
150+
async with get_client() as client:
151+
# Check if server is constrained to a specific project
152+
constrained_project = os.environ.get("BASIC_MEMORY_MCP_PROJECT")
153+
if constrained_project:
154+
return f"# Error\n\nProject deletion disabled - MCP server is constrained to project '{constrained_project}'.\nUse the CLI to delete projects: `basic-memory project remove \"{project_name}\"`"
155+
156+
if context: # pragma: no cover
157+
await context.info(f"Deleting project: {project_name}")
158+
159+
# Get project info before deletion to validate it exists
160+
response = await call_get(client, "/projects/projects")
161+
project_list = ProjectList.model_validate(response.json())
162+
163+
# Find the project by name (case-insensitive) or permalink - same logic as switch_project
164+
project_permalink = generate_permalink(project_name)
165+
target_project = None
166+
for p in project_list.projects:
167+
# Match by permalink (handles case-insensitive input)
168+
if p.permalink == project_permalink:
169+
target_project = p
170+
break
171+
# Also match by name comparison (case-insensitive)
172+
if p.name.lower() == project_name.lower():
173+
target_project = p
174+
break
175+
176+
if not target_project:
177+
available_projects = [p.name for p in project_list.projects]
178+
raise ValueError(
179+
f"Project '{project_name}' not found. Available projects: {', '.join(available_projects)}"
180+
)
181+
182+
# Call API to delete project using URL encoding for special characters
183+
from urllib.parse import quote
184+
185+
encoded_name = quote(target_project.name, safe="")
186+
response = await call_delete(client, f"/projects/{encoded_name}")
187+
status_response = ProjectStatusResponse.model_validate(response.json())
188+
189+
result = f"✓ {status_response.message}\n\n"
190+
191+
if status_response.old_project:
192+
result += "Removed project details:\n"
193+
result += f"• Name: {status_response.old_project.name}\n"
194+
if hasattr(status_response.old_project, "path"):
195+
result += f"• Path: {status_response.old_project.path}\n"
196+
197+
result += "Files remain on disk but project is no longer tracked by Basic Memory.\n"
198+
result += "Re-add the project to access its content again.\n"
199+
200+
return result

0 commit comments

Comments
 (0)