-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathtest_apps.py
More file actions
103 lines (83 loc) · 4.89 KB
/
Copy pathtest_apps.py
File metadata and controls
103 lines (83 loc) · 4.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
"""`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.client import advertise
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:
"""tutorial001: `@apps.tool(resource_uri=...)` stamps `_meta.ui.resourceUri` on the tool."""
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:
"""tutorial001: `add_html_resource` serves the HTML at `text/html;profile=mcp-app`,
the MIME type that tells a host "this is an app, render it"."""
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:
"""tutorial001: the canonical degradation pattern: raw data for a client that
negotiated Apps, a human sentence for one that did not."""
async with Client(
tutorial001.mcp, extensions=[advertise(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:
"""tutorial001: `main()` declares Apps support with the required `mimeTypes` and
receives the rich answer the page promises."""
await tutorial001.main()
assert "2026-06-26T12:00:00Z" in capsys.readouterr().out
async def test_capability_advertised_under_server_extensions() -> None:
"""tutorial001: passing `extensions=[apps]` advertises `io.modelcontextprotocol/ui`."""
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:
"""tutorial002: the iframe lockdown fields land under `_meta.ui` on both the list
entry and the read content item, with the spec's 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:
"""tutorial002: `visibility=["app"]` is metadata for the host; the server lists the
tool like any other and serves its calls. Filtering is the host's job."""
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:
"""tutorial003: `add_resource` accepts a pre-built `FileResource` and fills in the
`text/html;profile=mcp-app` MIME type the resource didn't set explicitly."""
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(encoding="utf-8")