@@ -254,23 +254,80 @@ async def direct_call_tool(
254254 ):
255255 # The MCP SDK wraps primitives and generic types like list in a `result` key, but we want to use the raw value returned by the tool function.
256256 # See https://github.com/modelcontextprotocol/python-sdk#structured-output
257- if isinstance (structured , dict ) and (
258- (len (structured ) == 1 and 'result' in structured )
259- or (len (structured ) == 2 and 'result' in structured and '_meta' in structured )
260- ):
257+ if isinstance (structured , dict ) and len (structured ) == 1 and 'result' in structured :
261258 return (
262- messages .ToolReturn (return_value = structured ['result' ], metadata = structured [ '_meta' ] )
263- if structured . get ( '_meta' , None ) is not None
259+ messages .ToolReturn (return_value = structured ['result' ], metadata = result . meta )
260+ if result . meta
264261 else structured ['result' ]
265262 )
263+ return messages .ToolReturn (return_value = structured , metadata = result .meta ) if result .meta else structured
264+
265+ mapped_part_metadata_tuple_list = [await self ._map_tool_result_part (part ) for part in result .content ]
266+ if (
267+ all (mapped_part_metadata_tuple [1 ] is None for mapped_part_metadata_tuple in mapped_part_metadata_tuple_list )
268+ and result .meta is None
269+ ):
270+ # There is no metadata in the tool result or its parts, return just the mapped values
266271 return (
267- messages .ToolReturn (return_value = structured , metadata = structured ['_meta' ])
268- if structured .get ('_meta' , None ) is not None
269- else structured
272+ mapped_part_metadata_tuple_list [0 ][0 ]
273+ if len (mapped_part_metadata_tuple_list [0 ]) == 1
274+ else [mapped_part_metadata_tuple [0 ] for mapped_part_metadata_tuple in mapped_part_metadata_tuple_list ]
275+ )
276+ elif (
277+ all (mapped_part_metadata_tuple [1 ] is None for mapped_part_metadata_tuple in mapped_part_metadata_tuple_list )
278+ and result .meta is not None
279+ ):
280+ # There is no metadata in the tool result parts, but there is metadata in the tool result
281+ return messages .ToolReturn (
282+ return_value = (
283+ mapped_part_metadata_tuple_list [0 ][0 ]
284+ if len (mapped_part_metadata_tuple_list [0 ]) == 1
285+ else [
286+ mapped_part_metadata_tuple [0 ] for mapped_part_metadata_tuple in mapped_part_metadata_tuple_list
287+ ]
288+ ),
289+ metadata = result .meta ,
290+ )
291+ else :
292+ # There is metadata in the tool result parts and there may be a metadata in the tool result, return a ToolReturn object
293+ return_values : list [Any ] = []
294+ user_contents : list [Any ] = []
295+ return_metadata : dict [str , Any ] = {}
296+ for idx , (mapped_part , part_metadata ) in enumerate (mapped_part_metadata_tuple_list ):
297+ if part_metadata is not None :
298+ # Merge the metadata dictionaries, with part metadata taking precedence
299+ if return_metadata .get ('content' , None ) is None :
300+ # Create an empty list if it doesn't exist yet
301+ return_metadata ['content' ] = list [dict [str , Any ]]()
302+ return_metadata ['content' ].append ({str (idx ): part_metadata })
303+ if isinstance (mapped_part , messages .BinaryContent ):
304+ identifier = mapped_part .identifier
305+
306+ return_values .append (f'See file { identifier } ' )
307+ user_contents .append ([f'This is file { identifier } :' , mapped_part ])
308+ else :
309+ user_contents .append (mapped_part )
310+
311+ if result .meta is not None and return_metadata .get ('content' , None ) is not None :
312+ # Merge the tool result metadata into the return metadata, with part metadata taking precedence
313+ return_metadata ['result' ] = result .meta
314+ elif result .meta is not None and return_metadata .get ('content' , None ) is None :
315+ return_metadata = result .meta
316+ elif (
317+ result .meta is None
318+ and return_metadata .get ('content' , None ) is not None
319+ and len (return_metadata ['content' ]) == 1
320+ ):
321+ # If there is only one content metadata, unwrap it
322+ return_metadata = return_metadata ['content' ][0 ]
323+ # TODO: What else should we cover here?
324+
325+ # Finally, construct and return the ToolReturn object
326+ return messages .ToolReturn (
327+ return_value = return_values ,
328+ content = user_contents ,
329+ metadata = return_metadata ,
270330 )
271-
272- mapped = [await self ._map_tool_result_part (part ) for part in result .content ]
273- return mapped [0 ] if len (mapped ) == 1 else mapped
274331
275332 async def call_tool (
276333 self ,
@@ -385,87 +442,32 @@ async def _sampling_callback(
385442
386443 async def _map_tool_result_part (
387444 self , part : mcp_types .ContentBlock
388- ) -> str | messages .ToolReturn | messages . BinaryContent | dict [str , Any ] | list [Any ]:
445+ ) -> tuple [ str | messages .BinaryContent | dict [str , Any ] | list [Any ], dict [ str , Any ] | None ]:
389446 # See https://github.com/jlowin/fastmcp/blob/main/docs/servers/tools.mdx#return-values
390447
391- # Let's also check for metadata but it can be present in not just TextContent
392448 metadata : dict [str , Any ] | None = part .meta
393449 if isinstance (part , mcp_types .TextContent ):
394450 text = part .text
395451 if text .startswith (('[' , '{' )):
396452 try :
397- return (
398- pydantic_core .from_json (text )
399- if metadata is None
400- else messages .ToolReturn (return_value = pydantic_core .from_json (text ), metadata = metadata )
401- )
453+ return pydantic_core .from_json (text ), metadata
402454 except ValueError :
403455 pass
404- return text if metadata is None else messages . ToolReturn ( return_value = text , metadata = metadata )
456+ return text , metadata
405457 elif isinstance (part , mcp_types .ImageContent ):
406- binary_response = messages .BinaryContent (data = base64 .b64decode (part .data ), media_type = part .mimeType )
407- return (
408- binary_response
409- if metadata is None
410- else messages .ToolReturn (
411- return_value = f'See file { binary_response .identifier } ' ,
412- content = [f'This is file { binary_response .identifier } :' , binary_response ],
413- metadata = metadata ,
414- )
415- )
416- elif isinstance (part , mcp_types .AudioContent ):
458+ return messages .BinaryContent (data = base64 .b64decode (part .data ), media_type = part .mimeType ), metadata
459+ elif isinstance (part , mcp_types .AudioContent ): # pragma: no cover
417460 # NOTE: The FastMCP server doesn't support audio content.
418461 # See <https://github.com/modelcontextprotocol/python-sdk/issues/952> for more details.
419- binary_response = messages .BinaryContent (data = base64 .b64decode (part .data ), media_type = part .mimeType )
420- return ( # pragma: no cover
421- binary_response
422- if metadata is None
423- else messages .ToolReturn (
424- return_value = f'See file { binary_response .identifier } ' ,
425- content = [f'This is file { binary_response .identifier } :' , binary_response ],
426- metadata = metadata ,
427- )
428- )
462+ return messages .BinaryContent (data = base64 .b64decode (part .data ), media_type = part .mimeType ), metadata
429463 elif isinstance (part , mcp_types .EmbeddedResource ):
430- resource = part .resource
431- response = self ._get_content (resource )
432- return (
433- response
434- if metadata is None
435- else messages .ToolReturn (
436- return_value = response if isinstance (response , str ) else f'See file { response .identifier } ' ,
437- content = None if isinstance (response , str ) else [f'This is file { response .identifier } :' , response ],
438- metadata = metadata ,
439- )
440- )
464+ return self ._get_content (part .resource ), metadata
441465 elif isinstance (part , mcp_types .ResourceLink ):
442466 resource_result : mcp_types .ReadResourceResult = await self ._client .read_resource (part .uri )
443467 if len (resource_result .contents ) == 1 :
444- response = self ._get_content (resource_result .contents [0 ])
445- return (
446- response
447- if metadata is None
448- else messages .ToolReturn (
449- return_value = response if isinstance (response , str ) else f'See file { response .identifier } ' ,
450- content = None
451- if isinstance (response , str )
452- else [f'This is file { response .identifier } :' , response ],
453- metadata = metadata ,
454- )
455- )
468+ return self ._get_content (resource_result .contents [0 ]), metadata
456469 else :
457- responses = [self ._get_content (resource ) for resource in resource_result .contents ] # pragma: no cover
458- return [ # pragma: no cover
459- response
460- if isinstance (response , str )
461- else messages .ToolReturn (
462- return_value = response if isinstance (response , str ) else f'See file { response .identifier } ' ,
463- content = None
464- if isinstance (response , str )
465- else [f'This is file { response .identifier } :' , response ],
466- )
467- for response in responses
468- ]
470+ return [self ._get_content (resource ) for resource in resource_result .contents ], metadata
469471 else :
470472 assert_never (part )
471473
@@ -941,7 +943,7 @@ def __eq__(self, value: object, /) -> bool:
941943 | messages .ToolReturn
942944 | dict [str , Any ]
943945 | list [Any ]
944- | Sequence [str | messages .BinaryContent | messages . ToolReturn | dict [str , Any ] | list [Any ]]
946+ | Sequence [str | messages .BinaryContent | dict [str , Any ] | list [Any ]]
945947)
946948"""The result type of an MCP tool call."""
947949
0 commit comments