Skip to content

Commit 53662c5

Browse files
fix: exclude None optional fields from task result serialization
model_dump() without exclude_none=True emits null for optional fields (e.g. TextContent.annotations), breaking Node SDK Zod validation which marks those fields as optional/absent rather than nullable. Matches the exclude_none=True, mode="json" convention used throughout the rest of the session layer's serialization paths. Fixes #2539
1 parent 3d7b311 commit 53662c5

2 files changed

Lines changed: 31 additions & 2 deletions

File tree

src/mcp/server/experimental/task_result_handler.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,11 @@ async def handle(
114114
# The stored result contains the actual payload data
115115
# Per spec: tasks/result MUST include _meta with related-task metadata
116116
related_task = RelatedTaskMetadata(task_id=task_id)
117-
related_task_meta: dict[str, Any] = {RELATED_TASK_METADATA_KEY: related_task.model_dump(by_alias=True)}
117+
related_task_meta: dict[str, Any] = {
118+
RELATED_TASK_METADATA_KEY: related_task.model_dump(by_alias=True, exclude_none=True)
119+
}
118120
if result is not None:
119-
result_data = result.model_dump(by_alias=True)
121+
result_data = result.model_dump(by_alias=True, mode="json", exclude_none=True)
120122
existing_meta: dict[str, Any] = result_data.get("_meta") or {}
121123
result_data["_meta"] = {**existing_meta, **related_task_meta}
122124
return GetTaskPayloadResult.model_validate(result_data)

tests/experimental/tasks/server/test_task_result_handler.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,33 @@ async def test_deliver_skips_resolver_registration_when_no_original_id(
291291
mock_session.send_message.assert_called_once()
292292

293293

294+
@pytest.mark.anyio
295+
async def test_handle_omits_none_optional_fields_in_result(
296+
store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler
297+
) -> None:
298+
"""None optional fields (e.g. TextContent.annotations) must be omitted, not serialized as null.
299+
300+
The Node SDK Zod schema marks these fields as optional (absent), not nullable,
301+
so sending null breaks validation.
302+
"""
303+
task = await store.create_task(TaskMetadata(ttl=60000), task_id="test-task")
304+
result = CallToolResult(content=[TextContent(type="text", text="hello")])
305+
await store.store_result(task.task_id, result)
306+
await store.update_task(task.task_id, status="completed")
307+
308+
mock_session = Mock()
309+
mock_session.send_message = AsyncMock()
310+
311+
request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task.task_id))
312+
response = await handler.handle(request, mock_session, "req-1")
313+
314+
wire_data = response.model_dump(by_alias=True, mode="json", exclude_none=True)
315+
content_items = wire_data.get("content", [])
316+
assert len(content_items) == 1
317+
assert "annotations" not in content_items[0]
318+
assert "_meta" not in content_items[0]
319+
320+
294321
@pytest.mark.anyio
295322
async def test_wait_for_task_update_handles_store_exception(
296323
store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler

0 commit comments

Comments
 (0)