Use 120 characters instead of 88 (#856)
This commit is contained in:
parent
f7265f7b91
commit
543961968c
9 changed files with 687 additions and 2142 deletions
|
|
@ -17,9 +17,7 @@ async def test_list_tools_returns_all_tools():
|
|||
f"""Tool number {i}"""
|
||||
return i
|
||||
|
||||
globals()[f"dummy_tool_{i}"] = (
|
||||
dummy_tool_func # Keep reference to avoid garbage collection
|
||||
)
|
||||
globals()[f"dummy_tool_{i}"] = dummy_tool_func # Keep reference to avoid garbage collection
|
||||
|
||||
# Get all tools
|
||||
tools = await mcp.list_tools()
|
||||
|
|
@ -30,6 +28,4 @@ async def test_list_tools_returns_all_tools():
|
|||
# Verify each tool is unique and has the correct name
|
||||
tool_names = [tool.name for tool in tools]
|
||||
expected_names = [f"tool_{i}" for i in range(num_tools)]
|
||||
assert sorted(tool_names) == sorted(
|
||||
expected_names
|
||||
), "Tool names don't match expected names"
|
||||
assert sorted(tool_names) == sorted(expected_names), "Tool names don't match expected names"
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ async def test_resource_templates():
|
|||
# Note: list_resource_templates() returns a decorator that wraps the handler
|
||||
# The handler returns a ServerResult with a ListResourceTemplatesResult inside
|
||||
result = await mcp._mcp_server.request_handlers[types.ListResourceTemplatesRequest](
|
||||
types.ListResourceTemplatesRequest(
|
||||
method="resources/templates/list", params=None
|
||||
)
|
||||
types.ListResourceTemplatesRequest(method="resources/templates/list", params=None)
|
||||
)
|
||||
assert isinstance(result.root, types.ListResourceTemplatesResult)
|
||||
templates = result.root.resourceTemplates
|
||||
|
|
|
|||
|
|
@ -61,9 +61,7 @@ async def test_resource_template_edge_cases():
|
|||
await mcp.read_resource("resource://users/123/posts") # Missing post_id
|
||||
|
||||
with pytest.raises(ValueError, match="Unknown resource"):
|
||||
await mcp.read_resource(
|
||||
"resource://users/123/posts/456/extra"
|
||||
) # Extra path component
|
||||
await mcp.read_resource("resource://users/123/posts/456/extra") # Extra path component
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
@ -110,11 +108,7 @@ async def test_resource_template_client_interaction():
|
|||
|
||||
# Verify invalid resource URIs raise appropriate errors
|
||||
with pytest.raises(Exception): # Specific exception type may vary
|
||||
await session.read_resource(
|
||||
AnyUrl("resource://users/123/posts")
|
||||
) # Missing post_id
|
||||
await session.read_resource(AnyUrl("resource://users/123/posts")) # Missing post_id
|
||||
|
||||
with pytest.raises(Exception): # Specific exception type may vary
|
||||
await session.read_resource(
|
||||
AnyUrl("resource://users/123/invalid")
|
||||
) # Invalid template
|
||||
await session.read_resource(AnyUrl("resource://users/123/invalid")) # Invalid template
|
||||
|
|
|
|||
|
|
@ -45,31 +45,19 @@ async def test_fastmcp_resource_mime_type():
|
|||
bytes_resource = mapping["test://image_bytes"]
|
||||
|
||||
# Verify mime types
|
||||
assert (
|
||||
string_resource.mimeType == "image/png"
|
||||
), "String resource mime type not respected"
|
||||
assert (
|
||||
bytes_resource.mimeType == "image/png"
|
||||
), "Bytes resource mime type not respected"
|
||||
assert string_resource.mimeType == "image/png", "String resource mime type not respected"
|
||||
assert bytes_resource.mimeType == "image/png", "Bytes resource mime type not respected"
|
||||
|
||||
# Also verify the content can be read correctly
|
||||
string_result = await client.read_resource(AnyUrl("test://image"))
|
||||
assert len(string_result.contents) == 1
|
||||
assert (
|
||||
getattr(string_result.contents[0], "text") == base64_string
|
||||
), "Base64 string mismatch"
|
||||
assert (
|
||||
string_result.contents[0].mimeType == "image/png"
|
||||
), "String content mime type not preserved"
|
||||
assert getattr(string_result.contents[0], "text") == base64_string, "Base64 string mismatch"
|
||||
assert string_result.contents[0].mimeType == "image/png", "String content mime type not preserved"
|
||||
|
||||
bytes_result = await client.read_resource(AnyUrl("test://image_bytes"))
|
||||
assert len(bytes_result.contents) == 1
|
||||
assert (
|
||||
base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes
|
||||
), "Bytes mismatch"
|
||||
assert (
|
||||
bytes_result.contents[0].mimeType == "image/png"
|
||||
), "Bytes content mime type not preserved"
|
||||
assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch"
|
||||
assert bytes_result.contents[0].mimeType == "image/png", "Bytes content mime type not preserved"
|
||||
|
||||
|
||||
async def test_lowlevel_resource_mime_type():
|
||||
|
|
@ -82,9 +70,7 @@ async def test_lowlevel_resource_mime_type():
|
|||
|
||||
# Create test resources with specific mime types
|
||||
test_resources = [
|
||||
types.Resource(
|
||||
uri=AnyUrl("test://image"), name="test image", mimeType="image/png"
|
||||
),
|
||||
types.Resource(uri=AnyUrl("test://image"), name="test image", mimeType="image/png"),
|
||||
types.Resource(
|
||||
uri=AnyUrl("test://image_bytes"),
|
||||
name="test image bytes",
|
||||
|
|
@ -101,9 +87,7 @@ async def test_lowlevel_resource_mime_type():
|
|||
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")
|
||||
]
|
||||
return [ReadResourceContents(content=bytes(image_bytes), mime_type="image/png")]
|
||||
raise Exception(f"Resource not found: {uri}")
|
||||
|
||||
# Test that resources are listed with correct mime type
|
||||
|
|
@ -119,28 +103,16 @@ async def test_lowlevel_resource_mime_type():
|
|||
bytes_resource = mapping["test://image_bytes"]
|
||||
|
||||
# Verify mime types
|
||||
assert (
|
||||
string_resource.mimeType == "image/png"
|
||||
), "String resource mime type not respected"
|
||||
assert (
|
||||
bytes_resource.mimeType == "image/png"
|
||||
), "Bytes resource mime type not respected"
|
||||
assert string_resource.mimeType == "image/png", "String resource mime type not respected"
|
||||
assert bytes_resource.mimeType == "image/png", "Bytes resource mime type not respected"
|
||||
|
||||
# Also verify the content can be read correctly
|
||||
string_result = await client.read_resource(AnyUrl("test://image"))
|
||||
assert len(string_result.contents) == 1
|
||||
assert (
|
||||
getattr(string_result.contents[0], "text") == base64_string
|
||||
), "Base64 string mismatch"
|
||||
assert (
|
||||
string_result.contents[0].mimeType == "image/png"
|
||||
), "String content mime type not preserved"
|
||||
assert getattr(string_result.contents[0], "text") == base64_string, "Base64 string mismatch"
|
||||
assert string_result.contents[0].mimeType == "image/png", "String content mime type not preserved"
|
||||
|
||||
bytes_result = await client.read_resource(AnyUrl("test://image_bytes"))
|
||||
assert len(bytes_result.contents) == 1
|
||||
assert (
|
||||
base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes
|
||||
), "Bytes mismatch"
|
||||
assert (
|
||||
bytes_result.contents[0].mimeType == "image/png"
|
||||
), "Bytes content mime type not preserved"
|
||||
assert base64.b64decode(getattr(bytes_result.contents[0], "blob")) == image_bytes, "Bytes mismatch"
|
||||
assert bytes_result.contents[0].mimeType == "image/png", "Bytes content mime type not preserved"
|
||||
|
|
|
|||
|
|
@ -35,15 +35,7 @@ async def test_progress_token_zero_first_call():
|
|||
await ctx.report_progress(10, 10) # Complete
|
||||
|
||||
# Verify progress notifications
|
||||
assert (
|
||||
mock_session.send_progress_notification.call_count == 3
|
||||
), "All progress notifications should be sent"
|
||||
mock_session.send_progress_notification.assert_any_call(
|
||||
progress_token=0, progress=0.0, total=10.0, message=None
|
||||
)
|
||||
mock_session.send_progress_notification.assert_any_call(
|
||||
progress_token=0, progress=5.0, total=10.0, message=None
|
||||
)
|
||||
mock_session.send_progress_notification.assert_any_call(
|
||||
progress_token=0, progress=10.0, total=10.0, message=None
|
||||
)
|
||||
assert mock_session.send_progress_notification.call_count == 3, "All progress notifications should be sent"
|
||||
mock_session.send_progress_notification.assert_any_call(progress_token=0, progress=0.0, total=10.0, message=None)
|
||||
mock_session.send_progress_notification.assert_any_call(progress_token=0, progress=5.0, total=10.0, message=None)
|
||||
mock_session.send_progress_notification.assert_any_call(progress_token=0, progress=10.0, total=10.0, message=None)
|
||||
|
|
|
|||
|
|
@ -66,9 +66,7 @@ async def test_request_id_match() -> None:
|
|||
)
|
||||
|
||||
await client_writer.send(SessionMessage(JSONRPCMessage(root=init_req)))
|
||||
response = (
|
||||
await server_reader.receive()
|
||||
) # Get init response but don't need to check it
|
||||
response = await server_reader.receive() # Get init response but don't need to check it
|
||||
|
||||
# Send initialized notification
|
||||
initialized_notification = JSONRPCNotification(
|
||||
|
|
@ -76,14 +74,10 @@ async def test_request_id_match() -> None:
|
|||
params=NotificationParams().model_dump(by_alias=True, exclude_none=True),
|
||||
jsonrpc="2.0",
|
||||
)
|
||||
await client_writer.send(
|
||||
SessionMessage(JSONRPCMessage(root=initialized_notification))
|
||||
)
|
||||
await client_writer.send(SessionMessage(JSONRPCMessage(root=initialized_notification)))
|
||||
|
||||
# Send ping request with custom ID
|
||||
ping_request = JSONRPCRequest(
|
||||
id=custom_request_id, method="ping", params={}, jsonrpc="2.0"
|
||||
)
|
||||
ping_request = JSONRPCRequest(id=custom_request_id, method="ping", params={}, jsonrpc="2.0")
|
||||
|
||||
await client_writer.send(SessionMessage(JSONRPCMessage(root=ping_request)))
|
||||
|
||||
|
|
@ -91,9 +85,7 @@ async def test_request_id_match() -> None:
|
|||
response = await server_reader.receive()
|
||||
|
||||
# Verify response ID matches request ID
|
||||
assert (
|
||||
response.message.root.id == custom_request_id
|
||||
), "Response ID should match request ID"
|
||||
assert response.message.root.id == custom_request_id, "Response ID should match request ID"
|
||||
|
||||
# Cancel server task
|
||||
tg.cancel_scope.cancel()
|
||||
|
|
|
|||
|
|
@ -47,11 +47,7 @@ async def test_server_base64_encoding_issue():
|
|||
# Register a resource handler that returns our test data
|
||||
@server.read_resource()
|
||||
async def read_resource(uri: AnyUrl) -> list[ReadResourceContents]:
|
||||
return [
|
||||
ReadResourceContents(
|
||||
content=binary_data, mime_type="application/octet-stream"
|
||||
)
|
||||
]
|
||||
return [ReadResourceContents(content=binary_data, mime_type="application/octet-stream")]
|
||||
|
||||
# Get the handler directly from the server
|
||||
handler = server.request_handlers[ReadResourceRequest]
|
||||
|
|
|
|||
|
|
@ -11,12 +11,7 @@ from anyio.abc import TaskStatus
|
|||
from mcp.client.session import ClientSession
|
||||
from mcp.server.lowlevel import Server
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.types import (
|
||||
AudioContent,
|
||||
EmbeddedResource,
|
||||
ImageContent,
|
||||
TextContent,
|
||||
)
|
||||
from mcp.types import AudioContent, EmbeddedResource, ImageContent, TextContent
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
@ -36,9 +31,7 @@ async def test_notification_validation_error(tmp_path: Path):
|
|||
slow_request_complete = anyio.Event()
|
||||
|
||||
@server.call_tool()
|
||||
async def slow_tool(
|
||||
name: str, arg
|
||||
) -> Sequence[TextContent | ImageContent | AudioContent | EmbeddedResource]:
|
||||
async def slow_tool(name: str, arg) -> Sequence[TextContent | ImageContent | AudioContent | EmbeddedResource]:
|
||||
nonlocal request_count
|
||||
request_count += 1
|
||||
|
||||
|
|
@ -75,9 +68,7 @@ async def test_notification_validation_error(tmp_path: Path):
|
|||
# - Long enough for fast operations (>10ms)
|
||||
# - Short enough for slow operations (<200ms)
|
||||
# - Not too short to avoid flakiness
|
||||
async with ClientSession(
|
||||
read_stream, write_stream, read_timeout_seconds=timedelta(milliseconds=50)
|
||||
) as session:
|
||||
async with ClientSession(read_stream, write_stream, read_timeout_seconds=timedelta(milliseconds=50)) as session:
|
||||
await session.initialize()
|
||||
|
||||
# First call should work (fast operation)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Claude Debug
|
||||
# Claude Debug
|
||||
"""Test for HackerOne vulnerability report #3156202 - malformed input DOS."""
|
||||
|
||||
import anyio
|
||||
|
|
@ -23,12 +23,8 @@ async def test_malformed_initialize_request_does_not_crash_server():
|
|||
instead of crashing the server (HackerOne #3156202).
|
||||
"""
|
||||
# Create in-memory streams for testing
|
||||
read_send_stream, read_receive_stream = anyio.create_memory_object_stream[
|
||||
SessionMessage | Exception
|
||||
](10)
|
||||
write_send_stream, write_receive_stream = anyio.create_memory_object_stream[
|
||||
SessionMessage
|
||||
](10)
|
||||
read_send_stream, read_receive_stream = anyio.create_memory_object_stream[SessionMessage | Exception](10)
|
||||
write_send_stream, write_receive_stream = anyio.create_memory_object_stream[SessionMessage](10)
|
||||
|
||||
try:
|
||||
# Create a malformed initialize request (missing required params field)
|
||||
|
|
@ -38,7 +34,7 @@ async def test_malformed_initialize_request_does_not_crash_server():
|
|||
method="initialize",
|
||||
# params=None # Missing required params field
|
||||
)
|
||||
|
||||
|
||||
# Wrap in session message
|
||||
request_message = SessionMessage(message=JSONRPCMessage(malformed_request))
|
||||
|
||||
|
|
@ -54,22 +50,22 @@ async def test_malformed_initialize_request_does_not_crash_server():
|
|||
):
|
||||
# Send the malformed request
|
||||
await read_send_stream.send(request_message)
|
||||
|
||||
|
||||
# Give the session time to process the request
|
||||
await anyio.sleep(0.1)
|
||||
|
||||
|
||||
# Check that we received an error response instead of a crash
|
||||
try:
|
||||
response_message = write_receive_stream.receive_nowait()
|
||||
response = response_message.message.root
|
||||
|
||||
|
||||
# Verify it's a proper JSON-RPC error response
|
||||
assert isinstance(response, JSONRPCError)
|
||||
assert response.jsonrpc == "2.0"
|
||||
assert response.id == "f20fe86132ed4cd197f89a7134de5685"
|
||||
assert response.error.code == INVALID_PARAMS
|
||||
assert "Invalid request parameters" in response.error.message
|
||||
|
||||
|
||||
# Verify the session is still alive and can handle more requests
|
||||
# Send another malformed request to confirm server stability
|
||||
another_malformed_request = JSONRPCRequest(
|
||||
|
|
@ -78,21 +74,19 @@ async def test_malformed_initialize_request_does_not_crash_server():
|
|||
method="tools/call",
|
||||
# params=None # Missing required params
|
||||
)
|
||||
another_request_message = SessionMessage(
|
||||
message=JSONRPCMessage(another_malformed_request)
|
||||
)
|
||||
|
||||
another_request_message = SessionMessage(message=JSONRPCMessage(another_malformed_request))
|
||||
|
||||
await read_send_stream.send(another_request_message)
|
||||
await anyio.sleep(0.1)
|
||||
|
||||
|
||||
# Should get another error response, not a crash
|
||||
second_response_message = write_receive_stream.receive_nowait()
|
||||
second_response = second_response_message.message.root
|
||||
|
||||
|
||||
assert isinstance(second_response, JSONRPCError)
|
||||
assert second_response.id == "test_id_2"
|
||||
assert second_response.error.code == INVALID_PARAMS
|
||||
|
||||
|
||||
except anyio.WouldBlock:
|
||||
pytest.fail("No response received - server likely crashed")
|
||||
finally:
|
||||
|
|
@ -109,12 +103,8 @@ async def test_multiple_concurrent_malformed_requests():
|
|||
Test that multiple concurrent malformed requests don't crash the server.
|
||||
"""
|
||||
# Create in-memory streams for testing
|
||||
read_send_stream, read_receive_stream = anyio.create_memory_object_stream[
|
||||
SessionMessage | Exception
|
||||
](100)
|
||||
write_send_stream, write_receive_stream = anyio.create_memory_object_stream[
|
||||
SessionMessage
|
||||
](100)
|
||||
read_send_stream, read_receive_stream = anyio.create_memory_object_stream[SessionMessage | Exception](100)
|
||||
write_send_stream, write_receive_stream = anyio.create_memory_object_stream[SessionMessage](100)
|
||||
|
||||
try:
|
||||
# Start a server session
|
||||
|
|
@ -136,18 +126,16 @@ async def test_multiple_concurrent_malformed_requests():
|
|||
method="initialize",
|
||||
# params=None # Missing required params
|
||||
)
|
||||
request_message = SessionMessage(
|
||||
message=JSONRPCMessage(malformed_request)
|
||||
)
|
||||
request_message = SessionMessage(message=JSONRPCMessage(malformed_request))
|
||||
malformed_requests.append(request_message)
|
||||
|
||||
|
||||
# Send all requests
|
||||
for request in malformed_requests:
|
||||
await read_send_stream.send(request)
|
||||
|
||||
|
||||
# Give time to process
|
||||
await anyio.sleep(0.2)
|
||||
|
||||
|
||||
# Verify we get error responses for all requests
|
||||
error_responses = []
|
||||
try:
|
||||
|
|
@ -156,10 +144,10 @@ async def test_multiple_concurrent_malformed_requests():
|
|||
error_responses.append(response_message.message.root)
|
||||
except anyio.WouldBlock:
|
||||
pass # No more messages
|
||||
|
||||
|
||||
# Should have received 10 error responses
|
||||
assert len(error_responses) == 10
|
||||
|
||||
|
||||
for i, response in enumerate(error_responses):
|
||||
assert isinstance(response, JSONRPCError)
|
||||
assert response.id == f"malformed_{i}"
|
||||
|
|
@ -169,4 +157,4 @@ async def test_multiple_concurrent_malformed_requests():
|
|||
await read_send_stream.aclose()
|
||||
await write_send_stream.aclose()
|
||||
await read_receive_stream.aclose()
|
||||
await write_receive_stream.aclose()
|
||||
await write_receive_stream.aclose()
|
||||
|
|
|
|||
Loading…
Reference in a new issue