diff --git a/src/fastmcp/server/context.py b/src/fastmcp/server/context.py index 18587bf14..cc66419e7 100644 --- a/src/fastmcp/server/context.py +++ b/src/fastmcp/server/context.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import warnings import weakref from collections.abc import Callable, Generator, Mapping, Sequence from contextlib import contextmanager @@ -26,6 +27,8 @@ from starlette.requests import Request from typing_extensions import TypeVar from uncalled_for import SharedContext +import fastmcp +from fastmcp.exceptions import FastMCPDeprecationWarning from fastmcp.resources.base import ResourceResult from fastmcp.server.elicitation import ( AcceptedElicitation, @@ -1130,9 +1133,11 @@ class Context: "value" field will be generated for the MCP interaction and automatically deconstructed into the primitive type upon response. - If the response_type is None, the generated schema will be that of an - empty object in order to comply with the MCP protocol requirements. - Clients must send an empty object ("{}")in response. + Passing ``response_type=None`` (or omitting it) is deprecated and will + be removed in a future version. The resulting empty-schema form-mode + request is ambiguous and causes some clients (e.g. VS Code) to hang on + an empty form. Pass an explicit ``response_type`` describing the data + you want back. Args: message: A human-readable message explaining what information is needed @@ -1153,6 +1158,17 @@ class Context: contexts. In background task mode (SEP-1686), it will set the task status to "input_required" and wait for the client to provide input. """ + if response_type is None and fastmcp.settings.deprecation_warnings: + warnings.warn( + "Calling ctx.elicit() without a response_type is deprecated " + "and will be removed in a future version. The empty-schema " + "form-mode request is ambiguous under the current MCP spec " + "and causes some clients (e.g. VS Code) to render an empty, " + "non-functional form. Pass an explicit response_type " + "describing the data you expect back.", + FastMCPDeprecationWarning, + stacklevel=2, + ) config = parse_elicit_response_type( response_type, response_title=response_title, diff --git a/tests/deprecated/test_elicitation.py b/tests/deprecated/test_elicitation.py new file mode 100644 index 000000000..d86e549d8 --- /dev/null +++ b/tests/deprecated/test_elicitation.py @@ -0,0 +1,29 @@ +"""Tests for deprecated elicitation behavior.""" + +from typing import Any, cast + +import pytest + +from fastmcp import Context, FastMCP +from fastmcp.client.client import Client +from fastmcp.client.elicitation import ElicitResult +from fastmcp.exceptions import FastMCPDeprecationWarning +from fastmcp.server.elicitation import AcceptedElicitation + + +async def test_elicitation_none_response_type_warns_deprecation(): + """Passing response_type=None is deprecated — warn at call time.""" + mcp = FastMCP("TestServer") + + @mcp.tool + async def my_tool(context: Context) -> dict[str, Any]: + with pytest.warns(FastMCPDeprecationWarning, match="response_type"): + result = await context.elicit(message="", response_type=None) + assert isinstance(result, AcceptedElicitation) + return cast(dict[str, Any], result.data) + + async def elicitation_handler(message, response_type, params, ctx): + return ElicitResult(action="accept", content={}) + + async with Client(mcp, elicitation_handler=elicitation_handler) as client: + await client.call_tool("my_tool", {})