|
9 | 9 | import anyio.streams.memory |
10 | 10 | import mcp_types as types |
11 | 11 | import pytest |
| 12 | +from inline_snapshot import snapshot |
12 | 13 | from mcp_types import ( |
13 | 14 | CONNECTION_CLOSED, |
14 | 15 | INTERNAL_ERROR, |
@@ -1828,6 +1829,70 @@ async def test_a_2026_result_type_tag_from_a_legacy_server_never_reaches_the_res |
1828 | 1829 | assert result.result_type == "complete" |
1829 | 1830 |
|
1830 | 1831 |
|
| 1832 | +# --- null structuredContent --- |
| 1833 | +# The results are scripted: a server built on this SDK leaves a null `structuredContent` off the wire. |
| 1834 | + |
| 1835 | + |
| 1836 | +def _listing_with_output_schema(output_schema: dict[str, Any]) -> dict[str, Any]: |
| 1837 | + tool = {"name": "t", "inputSchema": {"type": "object"}, "outputSchema": output_schema} |
| 1838 | + return {"resultType": "complete", "tools": [tool], "ttlMs": 0, "cacheScope": "private"} |
| 1839 | + |
| 1840 | + |
| 1841 | +@pytest.mark.anyio |
| 1842 | +async def test_call_tool_accepts_a_null_structured_content_the_output_schema_permits() -> None: |
| 1843 | + """Spec (2026-07-28): `structuredContent` may be any JSON value, null included, so a null |
| 1844 | + the output schema permits is a valid result rather than missing structured content.""" |
| 1845 | + dispatcher = _ScriptedDispatcher( |
| 1846 | + _discover_result_dict(), |
| 1847 | + _listing_with_output_schema({"type": ["object", "null"]}), |
| 1848 | + {"resultType": "complete", "content": [], "structuredContent": None}, |
| 1849 | + ) |
| 1850 | + with anyio.fail_after(5): |
| 1851 | + async with ClientSession(dispatcher=dispatcher) as session: |
| 1852 | + await session.discover() |
| 1853 | + await session.list_tools() |
| 1854 | + result = await session.call_tool("t", {}) |
| 1855 | + assert isinstance(result, CallToolResult) |
| 1856 | + assert result.structured_content is None |
| 1857 | + |
| 1858 | + |
| 1859 | +@pytest.mark.anyio |
| 1860 | +async def test_call_tool_rejects_a_null_structured_content_the_output_schema_forbids() -> None: |
| 1861 | + """A null `structuredContent` is validated like any other value: against a schema that does |
| 1862 | + not permit null it fails as a schema mismatch, not as missing structured content.""" |
| 1863 | + dispatcher = _ScriptedDispatcher( |
| 1864 | + _discover_result_dict(), |
| 1865 | + _listing_with_output_schema({"type": "object"}), |
| 1866 | + {"resultType": "complete", "content": [], "structuredContent": None}, |
| 1867 | + ) |
| 1868 | + with anyio.fail_after(5): |
| 1869 | + async with ClientSession(dispatcher=dispatcher) as session: |
| 1870 | + await session.discover() |
| 1871 | + await session.list_tools() |
| 1872 | + with pytest.raises(RuntimeError) as exc_info: |
| 1873 | + await session.call_tool("t", {}) |
| 1874 | + # Stable SDK prefix only: the message tail is jsonschema text that shifts with the dependency. |
| 1875 | + assert str(exc_info.value).startswith("Invalid structured content returned by tool t") |
| 1876 | + |
| 1877 | + |
| 1878 | +@pytest.mark.anyio |
| 1879 | +async def test_call_tool_reports_an_absent_structured_content_as_missing_even_when_null_is_permitted() -> None: |
| 1880 | + """SDK-defined: only a result with no `structuredContent` field at all is reported as |
| 1881 | + missing structured content, and it is so even when the output schema would accept null.""" |
| 1882 | + dispatcher = _ScriptedDispatcher( |
| 1883 | + _discover_result_dict(), |
| 1884 | + _listing_with_output_schema({"type": ["object", "null"]}), |
| 1885 | + {"resultType": "complete", "content": []}, |
| 1886 | + ) |
| 1887 | + with anyio.fail_after(5): |
| 1888 | + async with ClientSession(dispatcher=dispatcher) as session: |
| 1889 | + await session.discover() |
| 1890 | + await session.list_tools() |
| 1891 | + with pytest.raises(RuntimeError) as exc_info: |
| 1892 | + await session.call_tool("t", {}) |
| 1893 | + assert str(exc_info.value) == snapshot("Tool t has an output schema but did not return structured content") |
| 1894 | + |
| 1895 | + |
1831 | 1896 | @pytest.mark.anyio |
1832 | 1897 | async def test_session_call_tool_returns_input_required_result_when_opted_in() -> None: |
1833 | 1898 | """`ClientSession.call_tool(..., allow_input_required=True)` surfaces the |
|
0 commit comments