refactor: replace lowlevel Server decorators with on_* constructor kwargs (#1985)

This commit is contained in:
Max Isbey 2026-02-12 15:55:54 +00:00 • committed by GitHub
commit 0a22a9dc33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 3563 additions and 5481 deletions

View file

@ -1,15 +1,13 @@
import pytest
from mcp import types
from mcp import Client
from mcp.server.mcpserver import MCPServer
@pytest.mark.anyio
async def test_resource_templates():
# Create an MCP server
mcp = MCPServer("Demo")
# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str: # pragma: no cover
"""Get a personalized greeting"""
@ -20,23 +18,16 @@ async def test_resource_templates():
"""Dynamic user data"""
return f"Profile data for user {user_id}"
# Get the list of resource templates using the underlying server
# Note: list_resource_templates() returns a decorator that wraps the handler
# The handler returns a ServerResult with a ListResourceTemplatesResult inside
result = await mcp._lowlevel_server.request_handlers[types.ListResourceTemplatesRequest](
types.ListResourceTemplatesRequest(params=None)
)
assert isinstance(result, types.ListResourceTemplatesResult)
templates = result.resource_templates
async with Client(mcp) as client:
result = await client.list_resource_templates()
templates = result.resource_templates
# Verify we get both templates back
assert len(templates) == 2
assert len(templates) == 2
# Verify template details
greeting_template = next(t for t in templates if t.name == "get_greeting")
assert greeting_template.uri_template == "greeting://{name}"
assert greeting_template.description == "Get a personalized greeting"
greeting_template = next(t for t in templates if t.name == "get_greeting")
assert greeting_template.uri_template == "greeting://{name}"
assert greeting_template.description == "Get a personalized greeting"
profile_template = next(t for t in templates if t.name == "get_user_profile")
assert profile_template.uri_template == "users://{user_id}/profile"
assert profile_template.description == "Dynamic user data"
profile_template = next(t for t in templates if t.name == "get_user_profile")
assert profile_template.uri_template == "users://{user_id}/profile"
assert profile_template.description == "Dynamic user data"

View file

@ -3,9 +3,16 @@ import base64
import pytest
from mcp import Client, types
from mcp.server.lowlevel import Server
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server import Server, ServerRequestContext
from mcp.server.mcpserver import MCPServer
from mcp.types import (
BlobResourceContents,
ListResourcesResult,
PaginatedRequestParams,
ReadResourceRequestParams,
ReadResourceResult,
TextResourceContents,
)
pytestmark = pytest.mark.anyio
@ -58,7 +65,6 @@ async def test_mcpserver_resource_mime_type():
async def test_lowlevel_resource_mime_type():
"""Test that mime_type parameter is respected for resources."""
server = Server("test")
# Create a small test image as bytes
image_bytes = b"fake_image_data"
@ -74,17 +80,24 @@ async def test_lowlevel_resource_mime_type():
),
]
@server.list_resources()
async def handle_list_resources():
return test_resources
async def handle_list_resources(
ctx: ServerRequestContext, params: PaginatedRequestParams | None
) -> ListResourcesResult:
return ListResourcesResult(resources=test_resources)
@server.read_resource()
async def handle_read_resource(uri: str):
if str(uri) == "test://image":
return [ReadResourceContents(content=base64_string, mime_type="image/png")]
elif str(uri) == "test://image_bytes":
return [ReadResourceContents(content=bytes(image_bytes), mime_type="image/png")]
raise Exception(f"Resource not found: {uri}") # pragma: no cover
resource_contents: dict[str, list[TextResourceContents | BlobResourceContents]] = {
"test://image": [TextResourceContents(uri="test://image", text=base64_string, mime_type="image/png")],
"test://image_bytes": [
BlobResourceContents(
uri="test://image_bytes", blob=base64.b64encode(image_bytes).decode("utf-8"), mime_type="image/png"
)
],
}
async def handle_read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:
return ReadResourceResult(contents=resource_contents[str(params.uri)])
server = Server("test", on_list_resources=handle_list_resources, on_read_resource=handle_read_resource)
# Test that resources are listed with correct mime type
async with Client(server) as client:

View file

@ -13,8 +13,14 @@ These tests verify the fix works end-to-end through the JSON-RPC protocol.
import pytest
from mcp import Client, types
from mcp.server.lowlevel import Server
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server import Server, ServerRequestContext
from mcp.types import (
ListResourcesResult,
PaginatedRequestParams,
ReadResourceRequestParams,
ReadResourceResult,
TextResourceContents,
)
pytestmark = pytest.mark.anyio
@ -26,24 +32,24 @@ async def test_relative_uri_roundtrip():
the server would fail to serialize resources with relative URIs,
or the URI would be transformed during the roundtrip.
"""
server = Server("test")
@server.list_resources()
async def list_resources():
return [
types.Resource(name="user", uri="users/me"),
types.Resource(name="config", uri="./config"),
types.Resource(name="parent", uri="../parent/resource"),
]
async def handle_list_resources(
ctx: ServerRequestContext, params: PaginatedRequestParams | None
) -> ListResourcesResult:
return ListResourcesResult(
resources=[
types.Resource(name="user", uri="users/me"),
types.Resource(name="config", uri="./config"),
types.Resource(name="parent", uri="../parent/resource"),
]
)
@server.read_resource()
async def read_resource(uri: str):
return [
ReadResourceContents(
content=f"data for {uri}",
mime_type="text/plain",
)
]
async def handle_read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:
return ReadResourceResult(
contents=[TextResourceContents(uri=str(params.uri), text=f"data for {params.uri}", mime_type="text/plain")]
)
server = Server("test", on_list_resources=handle_list_resources, on_read_resource=handle_read_resource)
async with Client(server) as client:
# List should return the exact URIs we specified
@ -67,18 +73,23 @@ async def test_custom_scheme_uri_roundtrip():
Some MCP servers use custom schemes like "custom://resource".
These should work end-to-end.
"""
server = Server("test")
@server.list_resources()
async def list_resources():
return [
types.Resource(name="custom", uri="custom://my-resource"),
types.Resource(name="file", uri="file:///path/to/file"),
]
async def handle_list_resources(
ctx: ServerRequestContext, params: PaginatedRequestParams | None
) -> ListResourcesResult:
return ListResourcesResult(
resources=[
types.Resource(name="custom", uri="custom://my-resource"),
types.Resource(name="file", uri="file:///path/to/file"),
]
)
@server.read_resource()
async def read_resource(uri: str):
return [ReadResourceContents(content="data", mime_type="text/plain")]
async def handle_read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:
return ReadResourceResult(
contents=[TextResourceContents(uri=str(params.uri), text="data", mime_type="text/plain")]
)
server = Server("test", on_list_resources=handle_list_resources, on_read_resource=handle_read_resource)
async with Client(server) as client:
resources = await client.list_resources()

View file

@ -1,83 +1,52 @@
"""Test for base64 encoding issue in MCP server.
This test demonstrates the issue in server.py where the server uses
urlsafe_b64encode but the BlobResourceContents validator expects standard
base64 encoding.
The test should FAIL before fixing server.py to use b64encode instead of
urlsafe_b64encode.
After the fix, the test should PASS.
This test verifies that binary resource data is encoded with standard base64
(not urlsafe_b64encode), so BlobResourceContents validation succeeds.
"""
import base64
from typing import cast
import pytest
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server.lowlevel.server import Server
from mcp.types import (
BlobResourceContents,
ReadResourceRequest,
ReadResourceRequestParams,
ReadResourceResult,
ServerResult,
)
from mcp import Client
from mcp.server.mcpserver import MCPServer
from mcp.types import BlobResourceContents
pytestmark = pytest.mark.anyio
@pytest.mark.anyio
async def test_server_base64_encoding_issue():
"""Tests that server response can be validated by BlobResourceContents.
async def test_server_base64_encoding():
"""Tests that binary resource data round-trips correctly through base64 encoding.
This test will:
1. Set up a server that returns binary data
2. Extract the base64-encoded blob from the server's response
3. Verify the encoded data can be properly validated by BlobResourceContents
BEFORE FIX: The test will fail because server uses urlsafe_b64encode
AFTER FIX: The test will pass because server uses standard b64encode
The test uses binary data that produces different results with urlsafe vs standard
base64, ensuring the server uses standard encoding.
"""
server = Server("test")
mcp = MCPServer("test")
# Create binary data that will definitely result in + and / characters
# when encoded with standard base64
binary_data = bytes(list(range(255)) * 4)
# Register a resource handler that returns our test data
@server.read_resource()
async def read_resource(uri: str) -> list[ReadResourceContents]:
return [ReadResourceContents(content=binary_data, mime_type="application/octet-stream")]
# Get the handler directly from the server
handler = server.request_handlers[ReadResourceRequest]
# Create a request
request = ReadResourceRequest(
params=ReadResourceRequestParams(uri="test://resource"),
)
# Call the handler to get the response
result: ServerResult = await handler(request)
# After (fixed code):
read_result: ReadResourceResult = cast(ReadResourceResult, result)
blob_content = read_result.contents[0]
# First verify our test data actually produces different encodings
# Sanity check: our test data produces different encodings
urlsafe_b64 = base64.urlsafe_b64encode(binary_data).decode()
standard_b64 = base64.b64encode(binary_data).decode()
assert urlsafe_b64 != standard_b64, "Test data doesn't demonstrate"
" encoding difference"
assert urlsafe_b64 != standard_b64, "Test data doesn't demonstrate encoding difference"
# Now validate the server's output with BlobResourceContents.model_validate
# Before the fix: This should fail with "Invalid base64" because server
# uses urlsafe_b64encode
# After the fix: This should pass because server will use standard b64encode
model_dict = blob_content.model_dump()
@mcp.resource("test://binary", mime_type="application/octet-stream")
def get_binary() -> bytes:
"""Return binary test data."""
return binary_data
# Direct validation - this will fail before fix, pass after fix
blob_model = BlobResourceContents.model_validate(model_dict)
async with Client(mcp) as client:
result = await client.read_resource("test://binary")
assert len(result.contents) == 1
# Verify we can decode the data back correctly
decoded = base64.b64decode(blob_model.blob)
assert decoded == binary_data
blob_content = result.contents[0]
assert isinstance(blob_content, BlobResourceContents)
# Verify standard base64 was used (not urlsafe)
assert blob_content.blob == standard_b64
# Verify we can decode the data back correctly
decoded = base64.b64decode(blob_content.blob)
assert decoded == binary_data

View file

@ -1,8 +1,6 @@
"""Test to reproduce issue #88: Random error thrown on response."""
from collections.abc import Sequence
from pathlib import Path
from typing import Any
import anyio
import pytest
@ -11,10 +9,10 @@ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStre
from mcp import types
from mcp.client.session import ClientSession
from mcp.server.lowlevel import Server
from mcp.server import Server, ServerRequestContext
from mcp.shared.exceptions import MCPError
from mcp.shared.message import SessionMessage
from mcp.types import ContentBlock, TextContent
from mcp.types import CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent
@pytest.mark.anyio
@ -32,36 +30,38 @@ async def test_notification_validation_error(tmp_path: Path):
- Slow operations use minimal timeout (10ms) for quick test execution
"""
server = Server(name="test")
request_count = 0
slow_request_lock = anyio.Event()
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="slow",
description="A slow tool",
input_schema={"type": "object"},
),
types.Tool(
name="fast",
description="A fast tool",
input_schema={"type": "object"},
),
]
async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
return ListToolsResult(
tools=[
types.Tool(
name="slow",
description="A slow tool",
input_schema={"type": "object"},
),
types.Tool(
name="fast",
description="A fast tool",
input_schema={"type": "object"},
),
]
)
@server.call_tool()
async def slow_tool(name: str, arguments: dict[str, Any]) -> Sequence[ContentBlock]:
async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
nonlocal request_count
request_count += 1
assert params.name in ("slow", "fast"), f"Unknown tool: {params.name}"
if name == "slow":
if params.name == "slow":
await slow_request_lock.wait() # it should timeout here
return [TextContent(type="text", text=f"slow {request_count}")]
elif name == "fast":
return [TextContent(type="text", text=f"fast {request_count}")]
return [TextContent(type="text", text=f"unknown {request_count}")] # pragma: no cover
text = f"slow {request_count}"
else:
text = f"fast {request_count}"
return CallToolResult(content=[TextContent(type="text", text=text)])
server = Server(name="test", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)
async def server_handler(
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],