77import os
88from fastmcp import Context
99
10- from basic_memory .mcp .async_client import client
10+ from basic_memory .mcp .async_client import get_client
1111from basic_memory .mcp .server import mcp
1212from basic_memory .mcp .tools .utils import call_get , call_post , call_delete
1313from 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 \n Project creation disabled - MCP server is constrained to project \' { constrained_project } \' .\n Use 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 \n Project creation disabled - MCP server is constrained to project \' { constrained_project } \' .\n Use 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 += "\n Project 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 += "\n Project 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 \n Project deletion disabled - MCP server is constrained to project '{ constrained_project } '.\n Use 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 \n Project deletion disabled - MCP server is constrained to project '{ constrained_project } '.\n Use 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