PrefectHQ/fastmcp · error · TypeError

A completion handler returned a str; return a list of string

Error message

A completion handler returned a str; return a list of strings (for example, [value]) or a Completion instead.

What it means

A @mcp.completion handler returned a bare str, which normalize_completion deliberately rejects. A string satisfies Sequence[str] but would iterate into one-character 'candidates' on the wire, so FastMCP raises TypeError instead of silently producing garbage. Valid returns are mcp_types.Completion, list[str]/tuple[str], or None.

Source

Thrown at fastmcp_slim/fastmcp/server/completions.py:69

MAX_COMPLETION_VALUES = 100


def normalize_completion(result: CompletionValues) -> mcp_types.Completion:
    """Coerce a handler's return value into a wire ``Completion``.

    A returned ``str`` is rejected: it is almost always a mistake (the value
    would iterate into one-character candidates), so it raises rather than
    silently producing surprising output.

    The MCP contract caps a completion at 100 values, so a longer result is
    truncated to the first 100 with ``has_more`` set — a handler that returns
    thousands of matches emits a conforming response rather than an oversized
    one that strict clients reject.
    """
    if result is None:
        return mcp_types.Completion(values=[])
    if isinstance(result, str):
        raise TypeError(
            "A completion handler returned a str; return a list of strings "
            "(for example, [value]) or a Completion instead."
        )
    if isinstance(result, mcp_types.Completion):
        completion = result
    else:
        completion = mcp_types.Completion(values=list(result))

    if len(completion.values) > MAX_COMPLETION_VALUES:
        total = (
            completion.total if completion.total is not None else len(completion.values)
        )
        return mcp_types.Completion(
            values=completion.values[:MAX_COMPLETION_VALUES],
            total=total,
            has_more=True,
        )
    return completion

View on GitHub (pinned to 1f02114297)

Solutions

  1. Wrap the string in a list: return [value].
  2. Return None if there are no candidates.
  3. Return mcp_types.Completion(values=[...]) if you need total/has_more pagination hints.
  4. Annotate the handler's return type as CompletionValues (str is excluded) so type checkers catch this.

Example fix

// before
@mcp.completion
async def complete(ref, argument, context):
    return "my-value"
// after
@mcp.completion
async def complete(ref, argument, context):
    return ["my-value"]
Defensive patterns

Strategy: type-guard

Validate before calling

def check_completion_result(r):
    if r is None or isinstance(r, (mcp_types.Completion, list, tuple)):
        return r
    if isinstance(r, str):
        return [r]
    raise TypeError(f"invalid completion return: {type(r).__name__}")

Type guard

def is_valid_completion(r) -> bool:
    return r is None or isinstance(r, mcp_types.Completion) or (
        isinstance(r, (list, tuple)) and all(isinstance(v, str) for v in r)
    )

Try / catch

try:
    result = await handler(ref, arg, ctx)
except TypeError as e:
    logging.error("completion handler returned wrong type: %s", e)
    result = None  # empty completion

Prevention

When it happens

Trigger: Registering a completion handler via @mcp.completion whose return statement is `return value` (a single string) rather than a list, then receiving a completion/complete request that reaches the handler.

Common situations: Handlers that fetch a single best-match value and return it directly; authors misreading the return type and assuming a str is a valid single-candidate shorthand; refactors that changed a list return into a scalar return.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/60760a4f62d871db. Report an issue: GitHub.