Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/mcp/server/experimental/task_result_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,11 @@ 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(task_id=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, 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)
Expand Down
27 changes: 27 additions & 0 deletions tests/experimental/tasks/server/test_task_result_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,33 @@ async def test_deliver_skips_resolver_registration_when_no_original_id(
mock_session.send_message.assert_called_once()


@pytest.mark.anyio
async def test_handle_omits_none_optional_fields_in_result(
store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler
) -> None:
"""None optional fields (e.g. TextContent.annotations) must be omitted, not serialized as null.

The Node SDK Zod schema marks these fields as optional (absent), not nullable,
so sending null breaks validation.
"""
task = await store.create_task(TaskMetadata(ttl=60000), task_id="test-task")
result = CallToolResult(content=[TextContent(type="text", text="hello")])
await store.store_result(task.task_id, result)
await store.update_task(task.task_id, status="completed")

mock_session = Mock()
mock_session.send_message = AsyncMock()

request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task.task_id))
response = await handler.handle(request, mock_session, "req-1")

wire_data = response.model_dump(by_alias=True, mode="json", exclude_none=True)
content_items = wire_data.get("content", [])
assert len(content_items) == 1
assert "annotations" not in content_items[0]
assert "_meta" not in content_items[0]


@pytest.mark.anyio
async def test_wait_for_task_update_handles_store_exception(
store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler
Expand Down
Loading