@@ -64,6 +64,41 @@ async def _resolve_default_project_from_api() -> Optional[str]:
6464 return None
6565
6666
67+ async def _get_cached_active_project (context : Optional [Context ]) -> Optional [ProjectItem ]:
68+ """Return the cached active project from context when available."""
69+ if not context :
70+ return None
71+
72+ cached_raw = await context .get_state ("active_project" )
73+ if isinstance (cached_raw , dict ):
74+ return ProjectItem .model_validate (cached_raw )
75+ return None
76+
77+
78+ async def _set_cached_active_project (
79+ context : Optional [Context ],
80+ active_project : ProjectItem ,
81+ ) -> None :
82+ """Persist the active project and known default-project metadata in context."""
83+ if not context :
84+ return
85+
86+ await context .set_state ("active_project" , active_project .model_dump ())
87+ if active_project .is_default :
88+ await context .set_state ("default_project_name" , active_project .name )
89+
90+
91+ async def _get_cached_default_project (context : Optional [Context ]) -> Optional [str ]:
92+ """Return the cached default project name from context when available."""
93+ if not context :
94+ return None
95+
96+ cached_default = await context .get_state ("default_project_name" )
97+ if isinstance (cached_default , str ):
98+ return cached_default
99+ return None
100+
101+
67102def _canonicalize_project_name (
68103 project_name : Optional [str ],
69104 config : BasicMemoryConfig ,
@@ -85,11 +120,23 @@ def _canonicalize_project_name(
85120 return project_name
86121
87122
123+ def _project_matches_identifier (project_item : ProjectItem , identifier : Optional [str ]) -> bool :
124+ """Return True when the identifier refers to the cached project."""
125+ if identifier is None :
126+ return True
127+
128+ normalized_identifier = generate_permalink (identifier )
129+ return normalized_identifier in {
130+ generate_permalink (project_item .name ),
131+ project_item .permalink ,
132+ }
133+
88134
89135async def resolve_project_parameter (
90136 project : Optional [str ] = None ,
91137 allow_discovery : bool = False ,
92138 default_project : Optional [str ] = None ,
139+ context : Optional [Context ] = None ,
93140) -> Optional [str ]:
94141 """Resolve project parameter using unified linear priority chain.
95142
@@ -119,14 +166,32 @@ async def resolve_project_parameter(
119166 ):
120167 config = ConfigManager ().config
121168
122- # Load config for any values not explicitly provided.
123- # ConfigManager reads from the local config file, which doesn't exist in cloud mode.
124- # When it returns None, fall back to querying the projects API for the is_default flag.
125- if default_project is None :
169+ # Trigger: project already resolved earlier in the same MCP request
170+ # Why: the active project is request-constant, so re-discovering the
171+ # default project via /v2/projects/ just repeats work
172+ # Outcome: reuse the cached project name as the explicit candidate
173+ if project is None :
174+ cached_project = await _get_cached_active_project (context )
175+ if cached_project is not None :
176+ project = cached_project .name
177+
178+ # Trigger: there is no explicit project after env/context normalization
179+ # Why: default-project discovery is only needed as a fallback; doing it
180+ # for explicit requests adds an avoidable /v2/projects/ round-trip
181+ # Outcome: skip default lookup when the active project is already known
182+ if default_project is None and project is None :
183+ # Load config for any values not explicitly provided.
184+ # ConfigManager reads from the local config file, which doesn't exist in cloud mode.
185+ # When it returns None, fall back to querying the projects API for the is_default flag.
126186 default_project = config .default_project
127187
128- if default_project is None :
129- default_project = await _resolve_default_project_from_api ()
188+ if default_project is None :
189+ default_project = await _get_cached_default_project (context )
190+
191+ if default_project is None :
192+ default_project = await _resolve_default_project_from_api ()
193+ if default_project and context :
194+ await context .set_state ("default_project_name" , default_project )
130195
131196 # Create resolver with configuration and resolve
132197 resolver = ProjectResolver .from_env (
@@ -290,7 +355,12 @@ async def get_active_project(
290355 # Deferred import to avoid circular dependency with tools
291356 from basic_memory .mcp .tools .utils import call_post
292357
293- resolved_project = await resolve_project_parameter (project )
358+ cached_project = await _get_cached_active_project (context )
359+ if cached_project and _project_matches_identifier (cached_project , project ):
360+ logger .debug (f"Using cached project from context: { cached_project .name } " )
361+ return cached_project
362+
363+ resolved_project = await resolve_project_parameter (project , context = context )
294364 if not resolved_project :
295365 project_names = await get_project_names (client , headers )
296366 raise ValueError (
@@ -301,14 +371,9 @@ async def get_active_project(
301371
302372 project = resolved_project
303373
304- # Check if already cached in context
305- if context :
306- cached_raw = await context .get_state ("active_project" )
307- if isinstance (cached_raw , dict ):
308- cached_project = ProjectItem .model_validate (cached_raw )
309- if cached_project .name == project :
310- logger .debug (f"Using cached project from context: { project } " )
311- return cached_project
374+ if cached_project and _project_matches_identifier (cached_project , project ):
375+ logger .debug (f"Using cached project from context: { cached_project .name } " )
376+ return cached_project
312377
313378 # Validate project exists by calling API
314379 logger .debug (f"Validating project: { project } " )
@@ -328,8 +393,8 @@ async def get_active_project(
328393 )
329394
330395 # Cache in context if available
396+ await _set_cached_active_project (context , active_project )
331397 if context :
332- await context .set_state ("active_project" , active_project .model_dump ())
333398 logger .debug (f"Cached project in context: { project } " )
334399
335400 logger .debug (f"Validated project: { active_project .name } " )
@@ -383,6 +448,21 @@ async def resolve_project_and_path(
383448 # Why: allow project-scoped memory URLs without requiring a separate project parameter
384449 # Outcome: attempt to resolve the prefix as a project and route to it
385450 if project_prefix :
451+ cached_project = await _get_cached_active_project (context )
452+ if cached_project and _project_matches_identifier (cached_project , project_prefix ):
453+ resolved_project = await resolve_project_parameter (project_prefix , context = context )
454+ if resolved_project and generate_permalink (resolved_project ) != generate_permalink (
455+ project_prefix
456+ ):
457+ raise ValueError (
458+ f"Project is constrained to '{ resolved_project } ', cannot use '{ project_prefix } '."
459+ )
460+
461+ resolved_path = (
462+ f"{ cached_project .permalink } /{ remainder } " if include_project else remainder
463+ )
464+ return cached_project , resolved_path , True
465+
386466 try :
387467 from basic_memory .mcp .tools .utils import call_post
388468
@@ -397,7 +477,7 @@ async def resolve_project_and_path(
397477 if "project not found" not in str (exc ).lower ():
398478 raise
399479 else :
400- resolved_project = await resolve_project_parameter (project_prefix )
480+ resolved_project = await resolve_project_parameter (project_prefix , context = context )
401481 if resolved_project and generate_permalink (resolved_project ) != generate_permalink (
402482 project_prefix
403483 ):
@@ -412,8 +492,7 @@ async def resolve_project_and_path(
412492 path = resolved .path ,
413493 is_default = resolved .is_default ,
414494 )
415- if context :
416- await context .set_state ("active_project" , active_project .model_dump ())
495+ await _set_cached_active_project (context , active_project )
417496
418497 resolved_path = (
419498 f"{ resolved .permalink } /{ remainder } " if include_project else remainder
@@ -530,7 +609,7 @@ async def get_project_client(
530609 )
531610
532611 # Step 1: Resolve project name from config (no network call)
533- resolved_project = await resolve_project_parameter (project )
612+ resolved_project = await resolve_project_parameter (project , context = context )
534613 if not resolved_project :
535614 # Fall back to local client to discover projects and raise helpful error
536615 async with get_client () as client :
0 commit comments