1919 ActionSearch ,
2020 ResponseFunctionWebSearch ,
2121)
22+ from openai .types .responses .response_output_item import McpCall
2223from openai .types .responses .response_reasoning_item import (
2324 Content as ResponseReasoningTextContent ,
2425)
@@ -155,11 +156,7 @@ def get_developer_message(
155156 "web_search_preview" ,
156157 "code_interpreter" ,
157158 "container" ,
158- "mcp" ,
159159 ):
160- # These are built-in tools that are added to the system message.
161- # Adding in MCP for now until we support MCP tools executed
162- # server side
163160 pass
164161
165162 elif tool .type == "function" :
@@ -427,6 +424,44 @@ def _parse_final_message(message: Message) -> ResponseOutputItem:
427424 )
428425
429426
427+ def _parse_mcp_recipient (recipient : str ) -> tuple [str , str ]:
428+ """
429+ Parse MCP recipient into (server_label, tool_name).
430+
431+ For dotted recipients like "repo_browser.list":
432+ - server_label: "repo_browser" (namespace/server)
433+ - tool_name: "list" (specific tool)
434+
435+ For simple recipients like "filesystem":
436+ - server_label: "filesystem"
437+ - tool_name: "filesystem"
438+ """
439+ if "." in recipient :
440+ server_label = recipient .split ("." )[0 ]
441+ tool_name = recipient .split ("." )[- 1 ]
442+ else :
443+ server_label = recipient
444+ tool_name = recipient
445+ return server_label , tool_name
446+
447+
448+ def _parse_mcp_call (message : Message , recipient : str ) -> list [ResponseOutputItem ]:
449+ """Parse MCP calls into MCP call items."""
450+ server_label , tool_name = _parse_mcp_recipient (recipient )
451+ output_items = []
452+ for content in message .content :
453+ response_item = McpCall (
454+ arguments = content .text ,
455+ type = "mcp_call" ,
456+ name = tool_name ,
457+ server_label = server_label ,
458+ id = f"mcp_{ random_uuid ()} " ,
459+ status = "completed" ,
460+ )
461+ output_items .append (response_item )
462+ return output_items
463+
464+
430465def parse_output_message (message : Message ) -> list [ResponseOutputItem ]:
431466 """
432467 Parse a Harmony message into a list of output response items.
@@ -440,31 +475,35 @@ def parse_output_message(message: Message) -> list[ResponseOutputItem]:
440475 output_items : list [ResponseOutputItem ] = []
441476 recipient = message .recipient
442477
443- # Browser tool calls
444- if recipient is not None and recipient .startswith ("browser." ):
445- output_items .append (_parse_browser_tool_call (message , recipient ))
446-
447- # Analysis channel (reasoning/chain-of-thought)
448- elif message .channel == "analysis" :
449- output_items .extend (_parse_reasoning_content (message ))
478+ if recipient is not None :
479+ # Browser tool calls
480+ if recipient .startswith ("browser." ):
481+ output_items .append (_parse_browser_tool_call (message , recipient ))
450482
451- # Commentary channel
452- elif message .channel == "commentary" :
453- # Function calls
454- if recipient is not None and recipient .startswith ("functions." ):
483+ # Function calls (should only happen on commentary channel)
484+ elif message .channel == "commentary" and recipient .startswith ("functions." ):
455485 output_items .extend (_parse_function_call (message , recipient ))
456486
457487 # Built-in tools on commentary channel are treated as reasoning for now
458- elif recipient is not None and (
488+ elif (
459489 recipient .startswith ("python" )
460490 or recipient .startswith ("browser" )
461491 or recipient .startswith ("container" )
462492 ):
463493 output_items .extend (_parse_reasoning_content (message ))
494+
495+ # All other recipients are MCP calls
464496 else :
465- raise ValueError (f"Unknown recipient: { recipient } " )
497+ output_items .extend (_parse_mcp_call (message , recipient ))
498+
499+ # No recipient - handle based on channel for non-tool messages
500+ elif message .channel == "analysis" :
501+ output_items .extend (_parse_reasoning_content (message ))
502+
503+ elif message .channel == "commentary" :
504+ # Commentary channel without recipient shouldn't happen
505+ raise ValueError (f"Commentary channel message without recipient: { message } " )
466506
467- # Final output message
468507 elif message .channel == "final" :
469508 output_items .append (_parse_final_message (message ))
470509
@@ -483,20 +522,70 @@ def parse_remaining_state(parser: StreamableParser) -> list[ResponseOutputItem]:
483522 if current_recipient is not None and current_recipient .startswith ("browser." ):
484523 return []
485524
486- if parser .current_channel == "analysis" :
487- reasoning_item = ResponseReasoningItem (
488- id = f"rs_{ random_uuid ()} " ,
489- summary = [],
490- type = "reasoning" ,
491- content = [
492- ResponseReasoningTextContent (
493- text = parser .current_content , type = "reasoning_text"
525+ if current_recipient and parser .current_channel in ("commentary" , "analysis" ):
526+ if current_recipient .startswith ("functions." ):
527+ rid = random_uuid ()
528+ return [
529+ ResponseFunctionToolCall (
530+ arguments = parser .current_content ,
531+ call_id = f"call_{ rid } " ,
532+ type = "function_call" ,
533+ name = current_recipient .split ("." )[- 1 ],
534+ id = f"fc_{ rid } " ,
535+ status = "in_progress" ,
494536 )
495- ],
496- status = None ,
497- )
498- return [reasoning_item ]
499- elif parser .current_channel == "final" :
537+ ]
538+ # Built-in tools (python, browser, container) should be treated as reasoning
539+ elif not (
540+ current_recipient .startswith ("python" )
541+ or current_recipient .startswith ("browser" )
542+ or current_recipient .startswith ("container" )
543+ ):
544+ # All other recipients are MCP calls
545+ rid = random_uuid ()
546+ server_label , tool_name = _parse_mcp_recipient (current_recipient )
547+ return [
548+ McpCall (
549+ arguments = parser .current_content ,
550+ type = "mcp_call" ,
551+ name = tool_name ,
552+ server_label = server_label ,
553+ id = f"mcp_{ rid } " ,
554+ status = "in_progress" ,
555+ )
556+ ]
557+
558+ if parser .current_channel == "commentary" :
559+ return [
560+ ResponseReasoningItem (
561+ id = f"rs_{ random_uuid ()} " ,
562+ summary = [],
563+ type = "reasoning" ,
564+ content = [
565+ ResponseReasoningTextContent (
566+ text = parser .current_content , type = "reasoning_text"
567+ )
568+ ],
569+ status = None ,
570+ )
571+ ]
572+
573+ if parser .current_channel == "analysis" :
574+ return [
575+ ResponseReasoningItem (
576+ id = f"rs_{ random_uuid ()} " ,
577+ summary = [],
578+ type = "reasoning" ,
579+ content = [
580+ ResponseReasoningTextContent (
581+ text = parser .current_content , type = "reasoning_text"
582+ )
583+ ],
584+ status = None ,
585+ )
586+ ]
587+
588+ if parser .current_channel == "final" :
500589 output_text = ResponseOutputText (
501590 text = parser .current_content ,
502591 annotations = [], # TODO
@@ -513,6 +602,7 @@ def parse_remaining_state(parser: StreamableParser) -> list[ResponseOutputItem]:
513602 type = "message" ,
514603 )
515604 return [text_item ]
605+
516606 return []
517607
518608
0 commit comments