1616from mcp .client .stdio import stdio_client
1717
1818
19+ def _convert_schema_property (prop_schema : Dict [str , Any ]) -> Dict [str , Any ]:
20+ """Recursively convert a schema property, handling nested objects."""
21+ result = {
22+ "type" : prop_schema .get ("type" , "string" ),
23+ }
24+
25+ if "description" in prop_schema :
26+ result ["description" ] = prop_schema ["description" ]
27+
28+ # Handle nested objects
29+ if prop_schema .get ("type" ) == "object" and "properties" in prop_schema :
30+ nested_props = {}
31+ nested_required = []
32+
33+ for nested_name , nested_schema in prop_schema ["properties" ].items ():
34+ nested_props [nested_name ] = _convert_schema_property (nested_schema )
35+ if nested_name in prop_schema .get ("required" , []):
36+ nested_required .append (nested_name )
37+
38+ result ["properties" ] = nested_props
39+ if nested_required :
40+ result ["required" ] = nested_required
41+
42+ # Handle arrays of objects
43+ if prop_schema .get ("type" ) == "array" and "items" in prop_schema :
44+ result ["items" ] = _convert_schema_property (prop_schema ["items" ])
45+
46+ return result
47+
48+
1949def mcp_tool_to_openai_format (tool ) -> Dict [str , Any ]:
2050 """Convert MCP tool to OpenAI function calling format."""
2151 properties = {}
2252 required = []
2353
2454 if tool .inputSchema and "properties" in tool .inputSchema :
2555 for prop_name , prop_schema in tool .inputSchema ["properties" ].items ():
26- properties [prop_name ] = {
27- "type" : prop_schema .get ("type" , "string" ),
28- "description" : prop_schema .get ("description" , "" ),
29- }
56+ properties [prop_name ] = _convert_schema_property (prop_schema )
3057 if tool .inputSchema .get ("required" ) and prop_name in tool .inputSchema ["required" ]:
3158 required .append (prop_name )
3259
@@ -140,6 +167,43 @@ def extract_function_call(text: str):
140167 return None
141168
142169
170+ def _format_nested_properties (props : Dict [str , Any ], required : List [str ], prefix : str = "" , indent : int = 2 ) -> List [str ]:
171+ """Recursively format nested object properties with required field indicators."""
172+ param_list = []
173+ indent_str = " " * indent
174+
175+ for param_name , param_info in props .items ():
176+ param_type = param_info .get ('type' , 'string' )
177+ param_desc = param_info .get ('description' , '' )
178+ is_required = param_name in required
179+ full_param_name = f"{ prefix } .{ param_name } " if prefix else param_name
180+
181+ # Handle nested objects
182+ if param_type == 'object' and 'properties' in param_info :
183+ nested_props = param_info .get ('properties' , {})
184+ nested_required = param_info .get ('required' , [])
185+ req_mark = " (REQUIRED)" if is_required else ""
186+
187+ if param_desc :
188+ param_list .append (f"{ indent_str } { param_name } (object){ req_mark } : { param_desc } " )
189+ else :
190+ param_list .append (f"{ indent_str } { param_name } (object){ req_mark } " )
191+
192+ # Recursively format nested properties
193+ nested_params = _format_nested_properties (
194+ nested_props , nested_required , full_param_name , indent + 2
195+ )
196+ param_list .extend (nested_params )
197+ else :
198+ req_mark = " (REQUIRED)" if is_required else ""
199+ if param_desc :
200+ param_list .append (f"{ indent_str } { param_name } ({ param_type } ){ req_mark } : { param_desc } " )
201+ else :
202+ param_list .append (f"{ indent_str } { param_name } ({ param_type } ){ req_mark } " )
203+
204+ return param_list
205+
206+
143207def build_system_prompt (tools : list ) -> str :
144208 """Build system prompt from tool schemas."""
145209 tools_descriptions = []
@@ -151,34 +215,48 @@ def build_system_prompt(tools: list) -> str:
151215 props = params .get ('properties' , {})
152216 required = params .get ('required' , [])
153217
154- param_list = []
155- for param_name , param_info in props .items ():
156- param_type = param_info .get ('type' , 'string' )
157- param_desc = param_info .get ('description' , '' )
158- is_required = param_name in required
159- req_mark = " (required)" if is_required else ""
160- if param_desc :
161- param_list .append (f" { param_name } ({ param_type } ){ req_mark } : { param_desc } " )
162- else :
163- param_list .append (f" { param_name } ({ param_type } ){ req_mark } " )
218+ # Format parameters with nested object support
219+ param_list = _format_nested_properties (props , required )
164220
165221 params_str = "\n " .join (param_list ) if param_list else " (no parameters)"
166- tools_descriptions .append (f"{ name } : { desc } \n { params_str } " )
222+
223+ # Highlight required parameters at the top
224+ required_params = [p for p in required ]
225+ required_str = f"\n REQUIRED parameters: { ', ' .join (required_params )} " if required_params else ""
226+
227+ tools_descriptions .append (f"{ name } : { desc } { required_str } \n { params_str } " )
167228
168229 tools_list = "\n \n " .join ([f"{ i + 1 } . { td } " for i , td in enumerate (tools_descriptions )])
230+
231+ # Add example for create-event
232+ example_json = """{
233+ "name": "create-event",
234+ "arguments": {
235+ "calendarId": "primary",
236+ "summary": "Meeting",
237+ "start": "2025-01-01T10:00:00",
238+ "end": "2025-01-01T11:00:00"
239+ }
240+ }"""
241+
169242 return f"""You are a calendar assistant. When the user requests calendar actions, respond with ONLY a JSON object in this format:
170243
171244{{"name": "function_name", "arguments": {{"param": "value"}}}}
172245
246+ CRITICAL RULES:
247+ - You MUST include ALL required parameters (marked as REQUIRED)
248+ - For nested objects, ALL required fields within the object must be included
249+ - If a parameter is marked as REQUIRED, it cannot be omitted
250+ - Output ONLY valid JSON, no other text before or after
251+ - Use exact function and parameter names (case-sensitive)
252+
253+ Example for create-event:
254+ { example_json }
255+
173256Available functions:
174257{ tools_list }
175258
176- Rules:
177- - Output ONLY valid JSON, no other text
178- - Use exact function and parameter names (case-sensitive)
179- - Include all required parameters
180- - For calendar events, use create-event
181- - For current time, use get-current-time
259+ IMPORTANT: Before creating events, you may need to call get-current-time first to get accurate date/time context. Always include calendarId="primary" for create-event unless specified otherwise.
182260"""
183261
184262
@@ -320,10 +398,38 @@ async def call_agent(
320398 print ('[debug] calling function:' , func_name )
321399 func_result = await _execute_with_retry (session , func_name , func_args , tools )
322400 print ('[debug] func_result:' , func_result )
401+
402+ # Parse function result to extract success/error message
403+ result_message = ""
404+ try :
405+ result_data = json .loads (func_result ) if isinstance (func_result , str ) else func_result
406+ if result_data .get ('isError' , False ):
407+ # Extract error message
408+ if isinstance (result_data .get ('content' ), list ):
409+ for item in result_data ['content' ]:
410+ if item .get ('type' ) == 'text' :
411+ result_message = item .get ('text' , '' )
412+ break
413+ else :
414+ # Extract success message or summary
415+ if isinstance (result_data .get ('content' ), list ):
416+ for item in result_data ['content' ]:
417+ if item .get ('type' ) == 'text' :
418+ result_message = item .get ('text' , '' )
419+ break
420+ except Exception :
421+ result_message = str (func_result )
422+
323423 followup = conversation + [
424+ VlmChatMessage (role = "assistant" , contents = [VlmContent (type = "text" , text = response_text )]),
324425 VlmChatMessage (role = "user" , contents = [VlmContent (type = "text" ,
325- text = f"You called { func_name } with { func_args } . Result: { func_result } . "
326- f"Provide a natural language response. Do NOT call any function again." )])
426+ text = f"Function execution completed. Result: { result_message } \n \n "
427+ f"Now respond to the user in natural language. You are in RESPONSE MODE, not function calling mode.\n "
428+ f"- DO NOT output any JSON format\n "
429+ f"- DO NOT use {{}} brackets\n "
430+ f"- DO NOT call any function\n "
431+ f"- Just speak naturally like a helpful assistant\n "
432+ f"- Tell the user what happened with the calendar event in a friendly way" )])
327433 ]
328434 followup_response = ""
329435 for token in vlm .generate_stream (
0 commit comments