1b74b06753
Cut comment and docstring volume roughly in half across src, tests, examples, and docs_src: removed comments that restate the adjacent code, leftover development narration, section banners, and self-evident Args/Returns blocks, and compressed the remaining docstrings to a Google-style summary line plus only the detail that earns its place. Kept (and tightened) the load-bearing content: Raises sections, deprecation and version-availability notes, spec/RFC/issue references, why-comments for non-obvious decisions, and all coverage pragmas. The generated mcp_types.v* wire modules are untouched.
90 lines
4.0 KiB
Python
90 lines
4.0 KiB
Python
"""`docs/advanced/apps.md`: every claim the page makes, proved against the real SDK."""
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from mcp_types import TextContent, TextResourceContents
|
|
|
|
from docs_src.apps import tutorial001, tutorial002, tutorial003
|
|
from mcp import Client
|
|
from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID
|
|
|
|
# See test_index.py for why this is a per-module mark and not a conftest hook.
|
|
pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")]
|
|
|
|
|
|
async def test_the_tool_carries_the_ui_resource_reference() -> None:
|
|
async with Client(tutorial001.mcp) as client:
|
|
listed = await client.list_tools()
|
|
assert listed.tools[0].meta == {"ui": {"resourceUri": "ui://clock/app.html"}}
|
|
|
|
|
|
async def test_the_ui_resource_is_served_as_the_app_mime_type() -> None:
|
|
async with Client(tutorial001.mcp) as client:
|
|
result = await client.read_resource("ui://clock/app.html")
|
|
contents = result.contents[0]
|
|
assert isinstance(contents, TextResourceContents)
|
|
assert contents.mime_type == APP_MIME_TYPE
|
|
assert contents.text == tutorial001.CLOCK_HTML
|
|
|
|
|
|
async def test_one_tool_two_answers() -> None:
|
|
"""Degradation pattern: raw data for a client that negotiated Apps, a human sentence for one that did not."""
|
|
async with Client(tutorial001.mcp, extensions={EXTENSION_ID: {"mimeTypes": [APP_MIME_TYPE]}}) as ui_client:
|
|
rich = await ui_client.call_tool("get_time", {})
|
|
async with Client(tutorial001.mcp) as text_client:
|
|
plain = await text_client.call_tool("get_time", {})
|
|
assert rich.content == [TextContent(type="text", text="2026-06-26T12:00:00Z")]
|
|
assert plain.content == [TextContent(type="text", text="The time is 2026-06-26T12:00:00Z.")]
|
|
|
|
|
|
async def test_the_clock_client_program_runs_as_shown(capsys: pytest.CaptureFixture[str]) -> None:
|
|
await tutorial001.main()
|
|
assert "2026-06-26T12:00:00Z" in capsys.readouterr().out
|
|
|
|
|
|
async def test_capability_advertised_under_server_extensions() -> None:
|
|
async with Client(tutorial001.mcp) as client:
|
|
assert client.server_capabilities.extensions == {EXTENSION_ID: {}}
|
|
|
|
|
|
async def test_csp_permissions_domain_and_border_ride_the_resource_meta() -> None:
|
|
"""The fields land under `_meta.ui` on both the list entry and the read content, with camelCase wire keys."""
|
|
expected: dict[str, Any] = {
|
|
"ui": {
|
|
"csp": {"connectDomains": ["https://api.example.com"]},
|
|
"permissions": {"clipboardWrite": {}},
|
|
"domain": "dashboard.example.com",
|
|
"prefersBorder": True,
|
|
}
|
|
}
|
|
async with Client(tutorial002.mcp) as client:
|
|
listed = await client.list_resources()
|
|
result = await client.read_resource("ui://dashboard/app.html")
|
|
assert listed.resources[0].meta == expected
|
|
contents = result.contents[0]
|
|
assert isinstance(contents, TextResourceContents)
|
|
assert contents.meta == expected
|
|
|
|
|
|
async def test_an_app_only_tool_is_still_listed_and_callable() -> None:
|
|
"""`visibility` is metadata for the host — filtering is the host's job, not the server's."""
|
|
async with Client(tutorial002.mcp) as client:
|
|
listed = await client.list_tools()
|
|
result = await client.call_tool("refresh_dashboard", {})
|
|
assert listed.tools[0].meta == {"ui": {"resourceUri": "ui://dashboard/app.html", "visibility": ["app"]}}
|
|
assert result.content == [TextContent(type="text", text="refreshed")]
|
|
|
|
|
|
async def test_a_file_resource_is_served_with_the_app_mime_type_filled_in() -> None:
|
|
async with Client(tutorial003.mcp) as client:
|
|
listed = await client.list_tools()
|
|
called = await client.call_tool("refresh_report", {})
|
|
result = await client.read_resource("ui://report/app.html")
|
|
assert listed.tools[0].meta == {"ui": {"resourceUri": "ui://report/app.html"}}
|
|
assert called.content == [TextContent(type="text", text="report refreshed")]
|
|
contents = result.contents[0]
|
|
assert isinstance(contents, TextResourceContents)
|
|
assert contents.mime_type == APP_MIME_TYPE
|
|
assert contents.text == tutorial003.REPORT_HTML.read_text()
|