diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index 99b5a56c0a..8f974c3661 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -761,8 +761,20 @@ async def handler( ) -> CompleteResult: try: result = await func(params.ref, params.argument, params.context) + if result is None: + completion = Completion(values=[], total=None, has_more=None) + elif hasattr(result, "values") and len(result.values) > 100: + total = result.total if getattr(result, "total", None) is not None else len(result.values) + has_more = result.has_more if getattr(result, "has_more", None) is not None else True + completion = Completion( + values=result.values[:100], + total=total, + has_more=has_more, + ) + else: + completion = result return CompleteResult( - completion=result if result is not None else Completion(values=[], total=None, has_more=None), + completion=completion, ) except MCPError: raise diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 2b95a12290..9354e03ced 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -1849,6 +1849,57 @@ async def handle_completion( assert result.completion.values == ["bold", "italic", "underline"] +async def test_completion_handler_more_than_100_values_paginates() -> None: + """Issue #3649: When a completion handler returns >100 values, MCPServer caps values at 100 + and sets total to the full count and has_more=True across both modern and legacy sessions. + """ + mcp = MCPServer() + + @mcp.completion() + async def complete( + ref: PromptReference, argument: CompletionArgument, context: CompletionContext | None + ) -> Completion: + return Completion(values=[f"option-{i:03}" for i in range(150)]) + + ref = PromptReference(type="ref/prompt", name="test") + + # auto mode (2026-07-28 protocol with 100-item Field constraint) + async with Client(mcp, mode="auto") as client: + result = await client.complete(ref=ref, argument={"name": "field", "value": ""}) + assert len(result.completion.values) == 100 + assert result.completion.values[0] == "option-000" + assert result.completion.values[-1] == "option-099" + assert result.completion.total == 150 + assert result.completion.has_more is True + + # legacy mode (2025-11-25 protocol) + async with Client(mcp, mode="legacy") as client: + result = await client.complete(ref=ref, argument={"name": "field", "value": ""}) + assert len(result.completion.values) == 100 + assert result.completion.values[0] == "option-000" + assert result.completion.values[-1] == "option-099" + assert result.completion.total == 150 + assert result.completion.has_more is True + + +async def test_completion_handler_more_than_100_values_preserves_explicit_total_and_has_more() -> None: + """Issue #3649: When a handler returns >100 values with explicit total or has_more, those values win.""" + mcp = MCPServer() + + @mcp.completion() + async def complete( + ref: PromptReference, argument: CompletionArgument, context: CompletionContext | None + ) -> Completion: + return Completion(values=[f"item-{i}" for i in range(120)], total=500, has_more=False) + + ref = PromptReference(type="ref/prompt", name="test") + async with Client(mcp) as client: + result = await client.complete(ref=ref, argument={"name": "field", "value": ""}) + assert len(result.completion.values) == 100 + assert result.completion.total == 500 + assert result.completion.has_more is False + + async def test_custom_resource_returning_the_wrong_type_is_a_crash(caplog: pytest.LogCaptureFixture) -> None: """SDK-defined: a Resource subclass whose read() returns something other than str or bytes is the server's bug, so it is logged as a crash and answered with the generic -32603, not with