From 2be42bc5415c9d52ef2633e38a89851a88e5e0a7 Mon Sep 17 00:00:00 2001 From: Sheldon Date: Sun, 7 Jun 2026 13:08:31 +0800 Subject: [PATCH] Omit null fields from task result payloads --- .../experimental/task_result_handler.py | 10 ++++++-- .../tasks/server/test_task_result_handler.py | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/experimental/task_result_handler.py b/src/mcp/server/experimental/task_result_handler.py index 1cf7f69749..09f2a0a4e9 100644 --- a/src/mcp/server/experimental/task_result_handler.py +++ b/src/mcp/server/experimental/task_result_handler.py @@ -129,9 +129,15 @@ async def handle( # The stored result contains the actual payload data # Per spec: tasks/result MUST include _meta with related-task metadata related_task = RelatedTaskMetadata(taskId=task_id) - related_task_meta: dict[str, Any] = {RELATED_TASK_METADATA_KEY: related_task.model_dump(by_alias=True)} + related_task_meta: dict[str, Any] = { + RELATED_TASK_METADATA_KEY: related_task.model_dump( + by_alias=True, + mode="json", + exclude_none=True, + ) + } if result is not None: - result_data = result.model_dump(by_alias=True) + result_data = result.model_dump(by_alias=True, mode="json", exclude_none=True) existing_meta: dict[str, Any] = result_data.get("_meta") or {} result_data["_meta"] = {**existing_meta, **related_task_meta} return GetTaskPayloadResult.model_validate(result_data) diff --git a/tests/experimental/tasks/server/test_task_result_handler.py b/tests/experimental/tasks/server/test_task_result_handler.py index db5b9edc70..411d318ed1 100644 --- a/tests/experimental/tasks/server/test_task_result_handler.py +++ b/tests/experimental/tasks/server/test_task_result_handler.py @@ -67,6 +67,29 @@ async def test_handle_returns_result_for_completed_task( assert "io.modelcontextprotocol/related-task" in response.meta +@pytest.mark.anyio +async def test_handle_omits_none_fields_from_completed_task_payload( + store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler +) -> None: + """Test task result payloads omit optional None fields instead of serializing null.""" + task = await store.create_task(TaskMetadata(ttl=60000), task_id="test-task") + result = CallToolResult(content=[TextContent(type="text", text="Done!")]) + await store.store_result(task.taskId, result) + await store.update_task(task.taskId, status="completed") + + mock_session = Mock() + mock_session.send_message = AsyncMock() + + request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId=task.taskId)) + response = await handler.handle(request, mock_session, "req-1") + + payload = response.model_dump(by_alias=True, mode="json") + assert payload["content"] == [{"type": "text", "text": "Done!"}] + assert "annotations" not in payload["content"][0] + assert "_meta" not in payload["content"][0] + assert "io.modelcontextprotocol/related-task" in payload["_meta"] + + @pytest.mark.anyio async def test_handle_raises_for_nonexistent_task( store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler