49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
120 lines
5.1 KiB
Python
120 lines
5.1 KiB
Python
"""`docs/servers/resources.md`: every claim the page makes, proved against the real SDK."""
|
|
|
|
import base64
|
|
|
|
import pytest
|
|
from inline_snapshot import snapshot
|
|
from mcp_types import BlobResourceContents, Resource, ResourceTemplate, TextResourceContents
|
|
|
|
from docs_src.resources import tutorial001, tutorial002, tutorial003
|
|
from mcp import Client
|
|
from mcp.server import MCPServer
|
|
|
|
# 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_function_becomes_a_listed_resource() -> None:
|
|
"""tutorial001: the URI, the function name and the docstring are the whole listing entry."""
|
|
async with Client(tutorial001.mcp) as client:
|
|
(resource,) = (await client.list_resources()).resources
|
|
assert resource == snapshot(
|
|
Resource(
|
|
name="get_config",
|
|
uri="config://app",
|
|
description="The active shop configuration.",
|
|
mime_type="text/plain",
|
|
)
|
|
)
|
|
|
|
|
|
async def test_read_returns_the_return_value_as_text() -> None:
|
|
"""tutorial001: reading the URI runs the function and wraps the `str` in `TextResourceContents`."""
|
|
async with Client(tutorial001.mcp) as client:
|
|
result = await client.read_resource("config://app")
|
|
assert result.contents == [
|
|
TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")
|
|
]
|
|
|
|
|
|
async def test_template_is_listed_separately_from_resources() -> None:
|
|
"""tutorial002: a `{placeholder}` moves the entry from `resources/list` to `resources/templates/list`."""
|
|
async with Client(tutorial002.mcp) as client:
|
|
assert [r.uri for r in (await client.list_resources()).resources] == ["config://app"]
|
|
(template,) = (await client.list_resource_templates()).resource_templates
|
|
assert template == snapshot(
|
|
ResourceTemplate(
|
|
name="get_user_profile",
|
|
uri_template="users://{user_id}/profile",
|
|
description="A customer's profile.",
|
|
mime_type="text/plain",
|
|
)
|
|
)
|
|
|
|
|
|
async def test_reading_a_template_fills_the_placeholder() -> None:
|
|
"""tutorial002: the client reads a concrete URI; the matched value arrives as the function argument."""
|
|
async with Client(tutorial002.mcp) as client:
|
|
result = await client.read_resource("users://42/profile")
|
|
assert result.contents == [
|
|
TextResourceContents(
|
|
uri="users://42/profile", mime_type="text/plain", text="User 42: 12 orders since 2021."
|
|
)
|
|
]
|
|
|
|
|
|
def test_uri_params_must_match_function_params() -> None:
|
|
"""The `!!! check`: a placeholder/parameter mismatch is rejected at decoration time, not at read time."""
|
|
broken = MCPServer("Bookshop")
|
|
with pytest.raises(ValueError) as exc_info:
|
|
|
|
@broken.resource("users://{user_id}/profile")
|
|
def get_user_profile(user: str) -> None:
|
|
"""A customer's profile."""
|
|
|
|
assert str(exc_info.value) == snapshot(
|
|
"Mismatch between URI parameters {'user_id'} and function parameters {'user'}"
|
|
)
|
|
|
|
|
|
async def test_mime_type_is_what_you_declare() -> None:
|
|
"""tutorial003: `mime_type=` lands in the listing verbatim; the SDK never guesses it from the value."""
|
|
async with Client(tutorial003.mcp) as client:
|
|
resources = (await client.list_resources()).resources
|
|
assert {r.uri: r.mime_type for r in resources} == snapshot(
|
|
{
|
|
"docs://readme": "text/markdown",
|
|
"stats://catalog": "application/json",
|
|
"covers://placeholder": "image/gif",
|
|
}
|
|
)
|
|
|
|
|
|
async def test_str_return_is_sent_as_is() -> None:
|
|
"""tutorial003: a `str` return value is the text content, untouched."""
|
|
async with Client(tutorial003.mcp) as client:
|
|
(content,) = (await client.read_resource("docs://readme")).contents
|
|
assert isinstance(content, TextResourceContents)
|
|
assert content.text == "# Bookshop\n\nSearch the catalog with the `search_books` tool."
|
|
|
|
|
|
async def test_dict_return_becomes_json_text() -> None:
|
|
"""tutorial003: a non-`str`, non-`bytes` return value is serialised to JSON text."""
|
|
async with Client(tutorial003.mcp) as client:
|
|
(content,) = (await client.read_resource("stats://catalog")).contents
|
|
assert isinstance(content, TextResourceContents)
|
|
assert content.text == snapshot('{\n "books": 1204,\n "authors": 391\n}')
|
|
|
|
|
|
async def test_bytes_return_becomes_a_blob() -> None:
|
|
"""tutorial003: a `bytes` return value arrives as `BlobResourceContents`, base64-encoded in `blob`."""
|
|
async with Client(tutorial003.mcp) as client:
|
|
(content,) = (await client.read_resource("covers://placeholder")).contents
|
|
assert isinstance(content, BlobResourceContents)
|
|
assert content == BlobResourceContents(
|
|
uri="covers://placeholder",
|
|
mime_type="image/gif",
|
|
blob="R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
|
)
|
|
assert base64.b64decode(content.blob) == tutorial003.placeholder_cover()
|