Skip to content

A synchronous prompt callback returning Awaitable is rendered as coroutine text #3709

Open

Description

@inanna-malick

Initial Checks

  • Reproduced with the newest 2.x release, 2.3.0.
  • Searched the issue tracker for an existing report.

Release line

2.x (current stable)

Description

A synchronous prompt callback annotated Awaitable[list[str]] returned a coroutine that resolves to an empty message list. Rendering produced one user message containing the quoted coroutine representation instead of awaiting the result. An equivalent async def callback correctly returns no messages.

PromptResult explicitly includes Awaitable[SyncPromptResult], and Prompt.from_function() accepts the callback. I expected the returned awaitable to be resolved before message conversion. The current path checks whether the callback is async, runs a synchronous callback in a worker thread, then serializes its coroutine result as fallback text. This loses the callback's intended content even though registration succeeds.

The test closes the coroutine in finally to avoid an unawaited-coroutine warning and checks an equivalent async callback. This concerns awaiting a result, separately from #3643's handling of synchronous Sequence results. Related typing discussion: #2202.

Test: standalone failing reproduction, branch repro/prompt-awaitable-return (diff).

From that branch, after installing its development dependencies:

uv run --frozen pytest tests/server/mcpserver/prompts/test_base.py::test_prompt_render_awaits_awaitable_returned_by_sync_callback -q

Observed pytest failure:

assert sync_messages == []
E assert [Message(role..., meta=None))] == []
Left contains one more item: Message(... text='"<coroutine object ...>"' ...)

Example Code

from collections.abc import Awaitable

import anyio

from mcp.server.mcpserver import Context
from mcp.server.mcpserver.prompts.base import Prompt


async def main() -> None:
    async def resolve() -> list[str]:
        return []

    coroutine = resolve()

    def prompt() -> Awaitable[list[str]]:
        return coroutine

    try:
        messages = await Prompt.from_function(prompt).render(None, Context())
    finally:
        coroutine.close()
    assert messages == []


anyio.run(main)

Python & MCP Python SDK

Python 3.14.7 / Pydantic 2.12.5. Fails on latest stable mcp/mcp-types 2.3.0 (2118f14) and current main 754254c. Each targeted run: 1 intentional failure, 2 passing existing controls. The same test runs against each version’s source.

AI-assisted investigation and test reproduction, with a human in the loop.

Activity

  1. added
    bugSomething isn't working
    v2Affects the v2 line (2.x on main)
    on Oct 14, 2026
  2. leninworld commented on Oct 15, 2026

    @leninworld

    Thanks for the detailed reproduction. I reproduced this on current main (754254c08fe1c3eda18b69da3d336106fe8d72d6) using the locked project environment with CPython 3.12.14.

    A synchronous prompt callback returning an awaitable that resolves to [] produced one user message containing the coroutine representation instead of no messages. An awaitable resolving to ["hi"] is rendered the same way. The synchronous callback runs in the worker thread, and its result then goes straight to the InputRequiredResult and message-conversion checks without the awaitable being resolved, even though PromptResult permits Awaitable[SyncPromptResult].

    A bounded fix seems to be resolving an awaitable result once, after either callback path returns and before those checks. For tests, I’d cover awaitables resolving to [] and to ["hi"], and keep the existing synchronous worker-thread and async-callback tests.

    @inanna-malick, if you’d like to fix this yourself, please go ahead. Otherwise, if maintainers agree with this behavior and test scope and want an outside PR, I’d be glad to take it if assigned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions