chore: import upstream snapshot with attribution
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) Has been cancelled
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) Has been cancelled
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""`docs/handlers/lifespan.md`: every claim the page makes, proved against the real SDK."""
|
||||
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp_types import TextContent, TextResourceContents
|
||||
|
||||
from docs_src.lifespan import tutorial001, tutorial002
|
||||
from mcp import Client, MCPError
|
||||
from mcp.server import MCPServer
|
||||
from mcp.server.mcpserver import Context
|
||||
|
||||
# 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_lifespan_object_reaches_the_tool() -> None:
|
||||
"""tutorial001: the object the lifespan yields is `ctx.request_context.lifespan_context`."""
|
||||
async with Client(tutorial001.mcp) as client:
|
||||
result = await client.call_tool("count_books", {"genre": "poetry"})
|
||||
assert not result.is_error
|
||||
assert result.content == [TextContent(type="text", text="3 books in 'poetry'.")]
|
||||
assert result.structured_content == {"result": "3 books in 'poetry'."}
|
||||
|
||||
|
||||
async def test_context_parameter_never_reaches_the_input_schema() -> None:
|
||||
"""tutorial001: `ctx` is injected by the SDK, so `genre` is the only argument the model sees."""
|
||||
async with Client(tutorial001.mcp) as client:
|
||||
(tool,) = (await client.list_tools()).tools
|
||||
assert tool.input_schema == snapshot(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"genre": {"title": "Genre", "type": "string"}},
|
||||
"required": ["genre"],
|
||||
"title": "count_booksArguments",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def test_startup_runs_before_the_first_request_and_shutdown_after_the_last() -> None:
|
||||
"""tutorial002: `connect()` runs at startup, the `finally` runs `disconnect()` at shutdown."""
|
||||
assert not tutorial002.database.connected
|
||||
async with Client(tutorial002.mcp) as client:
|
||||
assert tutorial002.database.connected
|
||||
result = await client.call_tool("database_status", {})
|
||||
assert result.structured_content == {"result": "connected"}
|
||||
assert not tutorial002.database.connected
|
||||
|
||||
|
||||
async def test_bare_context_reaches_the_lifespan_object_in_resources_and_prompts() -> None:
|
||||
"""A resource or prompt declaring a bare `ctx: Context` gets the same lifespan object a tool gets."""
|
||||
mcp = MCPServer("Bookshop", lifespan=tutorial001.app_lifespan)
|
||||
|
||||
@mcp.resource("books://{genre}/count")
|
||||
def genre_count(genre: str, ctx: Context) -> str:
|
||||
"""Count the books in a genre."""
|
||||
app = ctx.request_context.lifespan_context
|
||||
assert isinstance(app, tutorial001.AppContext)
|
||||
return f"{app.db.query()} books in {genre!r}."
|
||||
|
||||
@mcp.prompt()
|
||||
def stock_report(ctx: Context) -> str:
|
||||
"""Ask for a stock report."""
|
||||
app = ctx.request_context.lifespan_context
|
||||
assert isinstance(app, tutorial001.AppContext)
|
||||
return f"Summarise a shelf of {app.db.query()} books."
|
||||
|
||||
async with Client(mcp) as client:
|
||||
resource = await client.read_resource("books://poetry/count")
|
||||
assert resource.contents == [
|
||||
TextResourceContents(uri="books://poetry/count", mime_type="text/plain", text="3 books in 'poetry'.")
|
||||
]
|
||||
prompt = await client.get_prompt("stock_report")
|
||||
(message,) = prompt.messages
|
||||
assert message.content == TextContent(type="text", text="Summarise a shelf of 3 books.")
|
||||
|
||||
|
||||
async def test_parameterized_context_is_tool_only(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""`Context[AppContext]` on a resource or prompt fails every call; the server logs the `ValueError`."""
|
||||
mcp = MCPServer("Bookshop", lifespan=tutorial001.app_lifespan)
|
||||
|
||||
@mcp.resource("books://{genre}/count")
|
||||
def genre_count(genre: str, ctx: Context[tutorial001.AppContext]) -> str:
|
||||
"""Count the books in a genre."""
|
||||
return f"{ctx.request_context.lifespan_context.db.query()} books in {genre!r}."
|
||||
|
||||
@mcp.prompt()
|
||||
def stock_report(ctx: Context[tutorial001.AppContext]) -> str:
|
||||
"""Ask for a stock report."""
|
||||
return f"Summarise a shelf of {ctx.request_context.lifespan_context.db.query()} books."
|
||||
|
||||
async with Client(mcp) as client:
|
||||
with pytest.raises(MCPError, match="Error creating resource from template"):
|
||||
await client.read_resource("books://poetry/count")
|
||||
assert "ValueError: Context is not available outside of a request" in caplog.text
|
||||
|
||||
caplog.clear()
|
||||
with pytest.raises(MCPError):
|
||||
await client.get_prompt("stock_report")
|
||||
assert "ValueError: Context is not available outside of a request" in caplog.text
|
||||
|
||||
|
||||
async def test_default_lifespan_yields_an_empty_dict() -> None:
|
||||
"""No `lifespan=`: the SDK's default yields `{}`, so `lifespan_context` is never `None`."""
|
||||
bare = MCPServer("Bare")
|
||||
|
||||
@bare.tool()
|
||||
def show(ctx: Context) -> str:
|
||||
"""Show the lifespan context."""
|
||||
return repr(ctx.request_context.lifespan_context)
|
||||
|
||||
async with Client(bare) as client:
|
||||
result = await client.call_tool("show", {})
|
||||
assert result.structured_content == {"result": "{}"}
|
||||
Reference in New Issue
Block a user