2727logger = logging .getLogger ("entities-mcp" )
2828
2929_client = None
30- _namespace_initialized = False
30+ _initialized_namespaces : set [ str ] = set ()
3131_client_init_lock = threading .Lock ()
3232
3333# Need to configure FastAPI separately and mount FastMCP on it
@@ -93,38 +93,70 @@ async def serve_spa(request: Request, catchall: str = ""):
9393def get_client () -> EvolveClient :
9494 """Get the EvolveClient singleton with lazy initialization.
9595
96- Initializes the client and ensures namespace exists on first access.
96+ Initializes the client and ensures the default namespace exists on first access.
9797 This avoids the FastMCP SSE lifespan initialization race condition.
9898 """
99- global _client , _namespace_initialized
99+ global _client
100100
101101 with _client_init_lock :
102102 if _client is None :
103103 logger .info ("Initializing Evolve client..." )
104104 _client = EvolveClient ()
105105 logger .info ("Evolve client initialized" )
106106
107- if not _namespace_initialized :
108- logger .info ("Ensuring namespace exists..." )
107+ default_ns = evolve_config .namespace_id
108+ if default_ns not in _initialized_namespaces :
109+ logger .info (f"Ensuring default namespace '{ default_ns } ' exists..." )
109110 try :
110- _client .ensure_namespace (evolve_config .namespace_id )
111- logger .info (f"Namespace '{ evolve_config .namespace_id } ' is ready" )
111+ _client .ensure_namespace (default_ns )
112+ _initialized_namespaces .add (default_ns )
113+ logger .info (f"Namespace '{ default_ns } ' is ready" )
112114 except Exception as e :
113- logger .error (f"Failed to ensure namespace '{ evolve_config . namespace_id } ': { e } " )
115+ logger .error (f"Failed to ensure namespace '{ default_ns } ': { e } " )
114116 raise
115- _namespace_initialized = True
116- logger .info ("Namespace initialization complete" )
117117
118118 return _client
119119
120120
121- def get_entities_logic (task : str , entity_type : str = "guideline" , include_public : bool = False , limit : int = 10 ) -> str :
122- """Implementation logic for get_entities tool."""
123- logger .info (f"Getting entities of type '{ entity_type } ' for task: { task } (include_public={ include_public } )" )
121+ def _resolve_namespace (namespace_id : str | None ) -> str :
122+ """Resolve the effective namespace, ensuring it exists before use."""
123+ resolved = namespace_id or evolve_config .namespace_id
124+ if resolved not in _initialized_namespaces :
125+ logger .info (f"Ensuring namespace '{ resolved } ' exists (first use)..." )
126+ try :
127+ get_client ().ensure_namespace (resolved )
128+ _initialized_namespaces .add (resolved )
129+ logger .info (f"Namespace '{ resolved } ' is ready" )
130+ except Exception as e :
131+ logger .error (f"Failed to ensure namespace '{ resolved } ': { e } " )
132+ raise
133+ return resolved
134+
135+
136+ def get_entities_logic (
137+ task : str ,
138+ entity_type : str = "guideline" ,
139+ include_public : bool = False ,
140+ limit : int = 10 ,
141+ user_id : str | None = None ,
142+ namespace_id : str | None = None ,
143+ session_id : str | None = None ,
144+ ) -> str :
145+ """Implementation logic for get_entities tool.
146+
147+ Retrieval is intentionally broad: user_id and session_id are NOT used as
148+ hard filters so that shared/older guidelines remain visible. They are
149+ accepted here for future opt-in narrowing but currently only logged.
150+ """
151+ resolved_ns = _resolve_namespace (namespace_id )
152+ logger .info (
153+ f"Getting entities of type '{ entity_type } ' for task: { task } "
154+ f"(namespace={ resolved_ns } , user_id={ user_id } , session_id={ session_id } , include_public={ include_public } )"
155+ )
124156 client = get_client ()
125157
126158 private_results = client .search_entities (
127- namespace_id = evolve_config . namespace_id ,
159+ namespace_id = resolved_ns ,
128160 query = task ,
129161 filters = {"type" : entity_type },
130162 limit = limit ,
@@ -137,13 +169,10 @@ def get_entities_logic(task: str, entity_type: str = "guideline", include_public
137169 response_lines .append (f"{ i } . { entity .content } " )
138170
139171 if include_public :
140- # Exclude the caller's own namespace: those entities are already in private_results,
141- # and using a bare entity ID for cross-namespace dedup risks false-positives when
142- # different namespaces share the same auto-assigned numeric IDs.
143172 public_results = client .get_public_entities (
144173 query = task ,
145174 entity_type = entity_type ,
146- exclude_namespace_ids = [evolve_config . namespace_id ],
175+ exclude_namespace_ids = [resolved_ns ],
147176 limit = limit ,
148177 )
149178 private_ids : set [str ] = {e .id for e in private_results }
@@ -161,7 +190,15 @@ def get_entities_logic(task: str, entity_type: str = "guideline", include_public
161190
162191
163192@mcp .tool ()
164- def get_entities (task : str , entity_type : str = "guideline" , include_public : bool = False , limit : int = 10 ) -> str :
193+ def get_entities (
194+ task : str ,
195+ entity_type : str = "guideline" ,
196+ include_public : bool = False ,
197+ limit : int = 10 ,
198+ user_id : str | None = None ,
199+ namespace_id : str | None = None ,
200+ session_id : str | None = None ,
201+ ) -> str :
165202 """
166203 Get relevant entities for a given task, filtered by type.
167204 Provide a task description and receive applicable best practices, guidelines, or policies.
@@ -171,64 +208,105 @@ def get_entities(task: str, entity_type: str = "guideline", include_public: bool
171208 entity_type: The type of entities to retrieve (e.g., 'guideline', 'policy'). Defaults to 'guideline'.
172209 include_public: If True, also include public entities from all namespaces. Defaults to False.
173210 limit: Maximum number of results to return from each source (private and public). Defaults to 10.
211+ user_id: Optional caller user ID. Logged for attribution; does not filter results.
212+ namespace_id: Optional namespace override. Falls back to the configured default.
213+ session_id: Optional session/thread ID. Logged for attribution; does not filter results.
174214 """
175- return get_entities_logic (task , entity_type , include_public , limit )
215+ return get_entities_logic (task , entity_type , include_public , limit , user_id , namespace_id , session_id )
176216
177217
178218@mcp .tool ()
179- def get_guidelines (task : str ) -> str :
219+ def get_guidelines (
220+ task : str ,
221+ user_id : str | None = None ,
222+ namespace_id : str | None = None ,
223+ session_id : str | None = None ,
224+ ) -> str :
180225 """
181226 Get relevant guidelines for a given task.
182227 Provide a task description and receive applicable best practices and guidelines.
183228 This tool is maintained for backward compatibility. Use 'get_entities' for more generic queries.
184229
185230 Args:
186231 task: A description of the task you want guidelines for
232+ user_id: Optional caller user ID. Logged for attribution; does not filter results.
233+ namespace_id: Optional namespace override. Falls back to the configured default.
234+ session_id: Optional session/thread ID. Logged for attribution; does not filter results.
187235 """
188- return get_entities_logic (task , "guideline" )
236+ return get_entities_logic (task , "guideline" , user_id = user_id , namespace_id = namespace_id , session_id = session_id )
189237
190238
191239@mcp .tool ()
192- def save_trajectory (trajectory_data : str , task_id : str | None = None , owner_id : str | None = None ) -> list [RecordedEntity ]:
240+ def save_trajectory (
241+ trajectory_data : str ,
242+ task_id : str | None = None ,
243+ owner_id : str | None = None ,
244+ user_id : str | None = None ,
245+ namespace_id : str | None = None ,
246+ session_id : str | None = None ,
247+ ) -> list [RecordedEntity ]:
193248 """
194249 Save the full agent trajectory to the Entity DB and generate guidelines
195250
196251 Args:
197252 trajectory_data: A JSON formatted OpenAI conversation.
198253 task_id: Optional identifier for the task.
199254 owner_id: Optional user ID to record as the owner of generated guidelines.
255+ user_id: Optional caller user ID. Attached as metadata to trajectory and guideline entities.
256+ namespace_id: Optional namespace override. Falls back to the configured default.
257+ session_id: Optional session/thread ID. Attached as metadata to trajectory and guideline entities.
200258 """
259+ resolved_ns = _resolve_namespace (namespace_id )
260+ # Prefer explicit user_id; fall back to owner_id for backward compatibility
261+ effective_user_id = user_id or owner_id
201262 task_id = task_id or str (uuid .uuid4 ())
263+
264+ logger .info (f"Saving trajectory: namespace={ resolved_ns } , user_id={ effective_user_id } , session_id={ session_id } , task_id={ task_id } " )
265+
202266 entities = []
203267 messages = json .loads (trajectory_data )
268+ trajectory_metadata_base : dict = {"task_id" : task_id }
269+ if effective_user_id :
270+ trajectory_metadata_base ["user_id" ] = effective_user_id
271+ if session_id :
272+ trajectory_metadata_base ["session_id" ] = session_id
273+
204274 for message in messages :
205275 entities .append (
206276 Entity (
207277 type = "trajectory" ,
208278 content = message ["content" ] if isinstance (message ["content" ], str ) else str (message ["content" ]),
209279 metadata = {
210- "task_id" : task_id ,
280+ ** trajectory_metadata_base ,
211281 "message" : message ,
212282 },
213283 )
214284 )
215285
216286 get_client ().update_entities (
217- namespace_id = evolve_config . namespace_id ,
287+ namespace_id = resolved_ns ,
218288 entities = entities ,
219289 enable_conflict_resolution = False ,
220290 )
221291 results = generate_guidelines (messages )
222292
293+ guideline_metadata_base : dict = {
294+ "source_task_id" : task_id ,
295+ "creation_mode" : "auto-mcp" ,
296+ }
297+ if effective_user_id :
298+ guideline_metadata_base ["owner_id" ] = effective_user_id
299+ guideline_metadata_base ["user_id" ] = effective_user_id
300+ if session_id :
301+ guideline_metadata_base ["session_id" ] = session_id
302+
223303 guideline_entities = [
224304 Entity (
225305 type = "guideline" ,
226306 content = guideline .content ,
227307 metadata = {
308+ ** guideline_metadata_base ,
228309 "task_description" : result .task_description ,
229- "source_task_id" : task_id ,
230- "creation_mode" : "auto-mcp" ,
231- ** ({"owner_id" : owner_id } if owner_id else {}),
232310 "category" : guideline .category ,
233311 "rationale" : guideline .rationale ,
234312 "trigger" : guideline .trigger ,
@@ -240,13 +318,13 @@ def save_trajectory(trajectory_data: str, task_id: str | None = None, owner_id:
240318 ]
241319 if guideline_entities :
242320 get_client ().update_entities (
243- namespace_id = evolve_config . namespace_id ,
321+ namespace_id = resolved_ns ,
244322 entities = guideline_entities ,
245323 enable_conflict_resolution = True ,
246324 )
247325
248326 return get_client ().search_entities (
249- namespace_id = evolve_config . namespace_id ,
327+ namespace_id = resolved_ns ,
250328 filters = {"type" : "trajectory" , "metadata.task_id" : task_id },
251329 limit = 1000 ,
252330 )
@@ -260,6 +338,7 @@ def create_entity(
260338 enable_conflict_resolution : bool = False ,
261339 owner_id : str | None = None ,
262340 visibility : str = "private" ,
341+ namespace_id : str | None = None ,
263342) -> str :
264343 """
265344 Create a single entity in the namespace.
@@ -271,11 +350,13 @@ def create_entity(
271350 enable_conflict_resolution: If True, uses LLM to check for conflicts with existing entities
272351 owner_id: Optional user ID to record as the owner of this entity
273352 visibility: Visibility of the entity — 'private' (default) or 'public'
353+ namespace_id: Optional namespace override. Falls back to the configured default.
274354
275355 Returns:
276356 JSON string with the entity update details (ADD/UPDATE/DELETE/NONE) and entity ID
277357 """
278- logger .info (f"Creating entity of type: { entity_type } " )
358+ resolved_ns = _resolve_namespace (namespace_id )
359+ logger .info (f"Creating entity of type: { entity_type } in namespace: { resolved_ns } " )
279360 try :
280361 if visibility not in ("private" , "public" ):
281362 return json .dumps ({"error" : f"Invalid visibility '{ visibility } ': must be 'private' or 'public'" })
@@ -312,7 +393,7 @@ def create_entity(
312393 entity = Entity (type = entity_type , content = content , metadata = metadata_dict )
313394
314395 updates = get_client ().update_entities (
315- namespace_id = evolve_config . namespace_id , entities = [entity ], enable_conflict_resolution = enable_conflict_resolution
396+ namespace_id = resolved_ns , entities = [entity ], enable_conflict_resolution = enable_conflict_resolution
316397 )
317398
318399 if updates :
@@ -332,22 +413,24 @@ def create_entity(
332413
333414
334415@mcp .tool ()
335- def publish_entity (entity_id : str , user_id : str | None = None ) -> str :
416+ def publish_entity (entity_id : str , user_id : str | None = None , namespace_id : str | None = None ) -> str :
336417 """
337418 Make an entity publicly visible to all users.
338419
339420 Args:
340421 entity_id: The ID of the entity to publish
341422 user_id: Caller identity; must match the entity's owner_id if one is set
423+ namespace_id: Optional namespace override. Falls back to the configured default.
342424
343425 Returns:
344426 JSON string with the updated entity, or an error message
345427 """
346- logger .info (f"publish entity={ entity_id } owner_present={ user_id is not None } namespace={ evolve_config .namespace_id } " )
428+ resolved_ns = _resolve_namespace (namespace_id )
429+ logger .info (f"publish entity={ entity_id } owner_present={ user_id is not None } namespace={ resolved_ns } " )
347430 try :
348431 from datetime import datetime , UTC
349432
350- entity = get_client ().get_entity_by_id (namespace_id = evolve_config . namespace_id , entity_id = entity_id )
433+ entity = get_client ().get_entity_by_id (namespace_id = resolved_ns , entity_id = entity_id )
351434 if entity is None :
352435 return json .dumps ({"error" : f"Entity { entity_id } not found" })
353436
@@ -362,7 +445,7 @@ def publish_entity(entity_id: str, user_id: str | None = None) -> str:
362445 if user_id is not None :
363446 metadata_updates ["owner_id" ] = user_id
364447 updated = get_client ().patch_entity_metadata (
365- namespace_id = evolve_config . namespace_id ,
448+ namespace_id = resolved_ns ,
366449 entity_id = entity_id ,
367450 metadata_updates = metadata_updates ,
368451 )
@@ -372,20 +455,22 @@ def publish_entity(entity_id: str, user_id: str | None = None) -> str:
372455
373456
374457@mcp .tool ()
375- def unpublish_entity (entity_id : str , user_id : str | None = None ) -> str :
458+ def unpublish_entity (entity_id : str , user_id : str | None = None , namespace_id : str | None = None ) -> str :
376459 """
377460 Revert an entity to private visibility.
378461
379462 Args:
380463 entity_id: The ID of the entity to unpublish
381464 user_id: Caller identity; must match the entity's owner_id if one is set
465+ namespace_id: Optional namespace override. Falls back to the configured default.
382466
383467 Returns:
384468 JSON string with the updated entity, or an error message
385469 """
386- logger .info (f"unpublish entity={ entity_id } namespace={ evolve_config .namespace_id } " )
470+ resolved_ns = _resolve_namespace (namespace_id )
471+ logger .info (f"unpublish entity={ entity_id } namespace={ resolved_ns } " )
387472 try :
388- entity = get_client ().get_entity_by_id (namespace_id = evolve_config . namespace_id , entity_id = entity_id )
473+ entity = get_client ().get_entity_by_id (namespace_id = resolved_ns , entity_id = entity_id )
389474 if entity is None :
390475 return json .dumps ({"error" : f"Entity { entity_id } not found" })
391476
@@ -394,7 +479,7 @@ def unpublish_entity(entity_id: str, user_id: str | None = None) -> str:
394479 return json .dumps ({"error" : "Permission denied: caller is not the owner of this entity" })
395480
396481 updated = get_client ().patch_entity_metadata (
397- namespace_id = evolve_config . namespace_id ,
482+ namespace_id = resolved_ns ,
398483 entity_id = entity_id ,
399484 metadata_updates = {"visibility" : "private" , "published_at" : None },
400485 )
@@ -404,21 +489,22 @@ def unpublish_entity(entity_id: str, user_id: str | None = None) -> str:
404489
405490
406491@mcp .tool ()
407- def delete_entity (entity_id : str ) -> str :
492+ def delete_entity (entity_id : str , namespace_id : str | None = None ) -> str :
408493 """
409494 Delete a specific entity by its ID.
410495
411496 Args:
412497 entity_id: The unique identifier of the entity to delete
498+ namespace_id: Optional namespace override. Falls back to the configured default.
413499
414500 Returns:
415501 JSON string confirming deletion or error message
416502 """
417- logger .info (f"Deleting entity: { entity_id } " )
503+ resolved_ns = _resolve_namespace (namespace_id )
504+ logger .info (f"Deleting entity: { entity_id } from namespace: { resolved_ns } " )
418505
419506 try :
420- # Use EvolveClient.delete_entity_by_id() to delete the entity
421- get_client ().delete_entity_by_id (namespace_id = evolve_config .namespace_id , entity_id = entity_id )
507+ get_client ().delete_entity_by_id (namespace_id = resolved_ns , entity_id = entity_id )
422508 return json .dumps ({"success" : True , "message" : f"Entity { entity_id } deleted successfully" })
423509 except EvolveException as e :
424510 logger .exception (f"Error deleting entity { entity_id } : { str (e )} " )
0 commit comments