@@ -52,6 +52,56 @@ def get_db_session():
5252 return Session (engine )
5353
5454
55+ def publish_event_sync (event_type : str , task : any , user_id : str , changes : list = None , task_before : dict = None ):
56+ """Publish event synchronously for MCP tools.
57+
58+ Used in sync MCP tools - runs the async event publishing in a thread
59+ with its own event loop. This works even when called from within
60+ an async context (like MCP tools called by OpenAI Agents SDK).
61+
62+ Args:
63+ event_type: Event type (created, updated, completed, deleted)
64+ task: Task SQLModel instance
65+ user_id: User who performed the action
66+ changes: List of field changes (for update events)
67+ task_before: Task state before changes (for update events)
68+ """
69+ from src .services .event_publisher import publish_task_event
70+ import threading
71+
72+ try :
73+ # Create a new thread with its own event loop to run async publishing
74+ # This works even when called from within an async context
75+ result = [None ]
76+ exception = [None ]
77+
78+ def run_in_thread ():
79+ try :
80+ loop = asyncio .new_event_loop ()
81+ asyncio .set_event_loop (loop )
82+ result [0 ] = loop .run_until_complete (
83+ publish_task_event (event_type , task , user_id , changes , task_before )
84+ )
85+ loop .close ()
86+ except Exception as e :
87+ exception [0 ] = e
88+
89+ thread = threading .Thread (target = run_in_thread , daemon = True )
90+ thread .start ()
91+ thread .join (timeout = 10 ) # Wait up to 10 seconds
92+
93+ if exception [0 ]:
94+ raise exception [0 ]
95+
96+ if result [0 ]:
97+ logger .debug (f"Event published synchronously: task.{ event_type } " )
98+ else :
99+ logger .warning (f"Event publishing returned False: task.{ event_type } " )
100+ except Exception as e :
101+ # Log error but don't fail the tool
102+ logger .error (f"Failed to publish event task.{ event_type } : { e } " , exc_info = True )
103+
104+
55105def fire_and_forget_event (coro ):
56106 """Run an async coroutine in the background (fire-and-forget).
57107
@@ -138,9 +188,8 @@ def add_task(
138188 session .commit ()
139189 session .refresh (task )
140190
141- # Phase V: Publish task.created event (fire-and-forget)
142- from src .services .event_publisher import publish_task_event
143- fire_and_forget_event (publish_task_event ("created" , task , user_id ))
191+ # Phase V: Publish task.created event synchronously (before returning)
192+ publish_event_sync ("created" , task , user_id )
144193
145194 # Calculate urgency for display
146195 urgency = calculate_urgency (task .due_date , task .timezone ) if task .due_date else None
@@ -252,15 +301,10 @@ def complete_task(
252301 session .refresh (updated_task )
253302
254303 # Phase V: Publish event based on completion state change
255- from src .services .event_publisher import publish_task_event , task_to_dict
256304 if updated_task .completed and not was_completed :
257- fire_and_forget_event ( publish_task_event ( "completed" , updated_task , user_id ) )
305+ publish_event_sync ( "completed" , updated_task , user_id )
258306 elif not updated_task .completed and was_completed :
259- fire_and_forget_event (publish_task_event (
260- "updated" , updated_task , user_id ,
261- changes = ["completed" ],
262- task_before = task_to_dict (task )
263- ))
307+ publish_event_sync ("updated" , updated_task , user_id , changes = ["completed" ])
264308
265309 return {
266310 "task_id" : updated_task .id ,
@@ -307,9 +351,8 @@ def delete_task(
307351 task_service .delete_task (task_id , user_id )
308352 session .commit ()
309353
310- # Phase V: Publish task.deleted event with task snapshot
311- from src .services .event_publisher import publish_task_event
312- fire_and_forget_event (publish_task_event ("deleted" , task_snapshot , user_id ))
354+ # Phase V: Publish task.deleted event with task snapshot (synchronous)
355+ publish_event_sync ("deleted" , task_snapshot , user_id )
313356
314357 return {
315358 "task_id" : task_id ,
@@ -404,12 +447,8 @@ def update_task(
404447 session .commit ()
405448 session .refresh (updated_task )
406449
407- # Phase V: Publish task.updated event with before/after state
408- fire_and_forget_event (publish_task_event (
409- "updated" , updated_task , user_id ,
410- changes = changes ,
411- task_before = task_before_dict
412- ))
450+ # Phase V: Publish task.updated event with before/after state (synchronous)
451+ publish_event_sync ("updated" , updated_task , user_id , changes , task_before_dict )
413452
414453 # Calculate urgency for display
415454 urgency = calculate_urgency (updated_task .due_date , updated_task .timezone ) if updated_task .due_date else None
0 commit comments