Hacktoberfest 2026: die Issues, die Maintainer für den Oktober markiert haben – offen und einsteigerfreundlich. Hacktoberfest-Issues durchsuchen

A tool returning an empty list produces a CallToolResult with zero content blocks

Offen
#3,305 1 Kommentar 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Bewertung

Schwierigkeit
2/5
Geschätzter Aufwand
1-3 Stunden
Anfängerfreundlichkeit
55/100
Issue-Typ
Bug
Klarheit
Klar beschrieben
Aktivitätsstatus
Ruhig
Tech-Stack
python
Bereich
api

Rechercherichtung

Beginne mit _convert_to_content in src/mcp/server/mcpserver/utilities/func_metadata.py:563 und vergleiche dessen Verhalten bei leeren Listen mit dem dokumentierten Pfad für unstrukturierte Inhalte. Verwende die Reproduktionen find_person und find_two aus dem Issue, um zu überprüfen, dass nichtleere Ergebnisse unverändert bleiben und dass das vereinbarte Verhalten für eine leere Liste abgedeckt ist.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

enhancement needs decision P2 v1 v2
Initial Checks
  • I confirm that I'm using the newest release of my line (mcp 2.0.0)
  • I confirm that I searched for my issue before opening this issue
Release line

2.x

Description

A tool that returns an empty list produces a CallToolResult with zero content blocks. For a client that reads unstructured content, "no matches" and "the call returned nothing" become the same thing.

_convert_to_content (src/mcp/server/mcpserver/utilities/func_metadata.py:563) flattens a list by concatenating the conversion of each item:

if isinstance(result, list | tuple):
    return list(chain.from_iterable(_convert_to_content(item) for item in result))

With zero items there is nothing to concatenate, so the result is []. That is a natural consequence of the flattening rule rather than an oversight, but it produces an asymmetry that is surprising in practice — the same "empty" answer behaves differently depending on the return type:

tool returns content blocks text a client sees
[] 0 ''
[{...}, {...}] 2 both items
"" 1 ''

structuredContent is populated correctly in all three cases ({'result': []} for the empty list), so a client that reads structured output is unaffected. The gap is specific to clients consuming unstructured content — which is the population _convert_to_content's own docstring says it exists to serve: "retained for purposes of backwards compatibility."

Why this matters in practice. We hit it with an LLM-facing directory search. A find_person tool that legitimately found nobody returned an empty content array, and from the model's side that is indistinguishable from a call that produced nothing — so it re-tried the same lookup with cosmetic variations instead of concluding the person did not exist. Observed in one turn: find_person("Martha") → nothing → find_person("Martha Highlander") → nothing → find_person("Martha") again. Three identical searches, no answer. We work around it today by patching _convert_to_content in our own server so an empty list serializes to one TextContent holding [].

Worth noting the SDK already states this rule for tool authors — the docs advise returning [] rather than "" for an empty result — but a tool that follows that advice is exactly the one whose content array comes back empty.

Possible fix. In the list branch, when the conversion yields no blocks, emit a single TextContent with the serialized empty collection rather than an empty list. That keeps every non-empty case byte-identical and changes only the currently-empty one. I have not opened a PR: it changes output for every list-returning tool on the unstructured path, so it seemed worth agreeing on the direction (and on whether the empty-content array is considered correct as-is) before writing code. Happy to put one up if you'd like it.

Example Code
"""A tool returning an empty list yields a CallToolResult with zero content blocks."""

import asyncio

from mcp import Client
from mcp.client._memory import InMemoryTransport
from mcp.server.mcpserver import MCPServer

server = MCPServer("repro")


@server.tool()
def find_person(name: str) -> list[dict]:
    """Return matching people; an empty list means no match."""
    return []


@server.tool()
def find_two(name: str) -> list[dict]:
    """Two matches, for contrast."""
    return [{"name": "a"}, {"name": "b"}]


@server.tool()
def find_person_str(name: str) -> str:
    """The same answer as a string, for comparison."""
    return ""


async def main() -> None:
    async with Client(InMemoryTransport(server)) as client:
        for tool in ("find_person", "find_two", "find_person_str"):
            result = await client.call_tool(tool, {"name": "Martha"})
            text = "".join(b.text for b in result.content if b.type == "text")
            print(f"{tool}:")
            print(f"  content blocks    = {len(result.content)}")
            print(f"  text seen by model= {text!r}")
            print(f"  structuredContent = {result.structured_content!r}")


asyncio.run(main())

Output:

find_person:
  content blocks    = 0
  text seen by model= ''
  structuredContent = {'result': []}
find_two:
  content blocks    = 2
  text seen by model= '{\n  "name": "a"\n}{\n  "name": "b"\n}'
  structuredContent = {'result': [{'name': 'a'}, {'name': 'b'}]}
find_person_str:
  content blocks    = 1
  text seen by model= ''
  structuredContent = {'result': ''}
Python & MCP Python SDK
python 3.14.3
mcp 2.0.0
platform macOS-15.6.1-arm64-arm-64bit-Mach-O

AI assistance disclosure, per CONTRIBUTING: this issue was investigated and drafted with Claude Code. The repro above was run and its output pasted verbatim, and I reviewed the whole thing before filing.

Vorherrschende Sprache
Python
Sterne
24.3k
Forks
4k
Ø Merge
1 T. 11 Std.
Gemergte PRs (30 T.)
30

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus modelcontextprotocol/python-sdk

Alle Issues in modelcontextprotocol/python-sdk

Ähnliche Issues

Weitere Issues zu Python

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.