Add pyright strict mode on the whole project (#1254)

This commit is contained in:
Marcelo Trylesinski 2025-08-11 19:56:37 +02:00 • committed by GitHub
commit c7671e470c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 652 additions and 674 deletions

View file

@ -1,10 +1,11 @@
import subprocess
import sys
from pathlib import Path
from typing import Any
import pytest
from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path
from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path # type: ignore[reportPrivateUsage]
@pytest.mark.parametrize(
@ -14,7 +15,7 @@ from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path
("foo.py:srv_obj", "srv_obj"),
],
)
def test_parse_file_path_accepts_valid_specs(tmp_path, spec, expected_obj):
def test_parse_file_path_accepts_valid_specs(tmp_path: Path, spec: str, expected_obj: str | None):
"""Should accept valid file specs."""
file = tmp_path / spec.split(":")[0]
file.write_text("x = 1")
@ -23,13 +24,13 @@ def test_parse_file_path_accepts_valid_specs(tmp_path, spec, expected_obj):
assert obj == expected_obj
def test_parse_file_path_missing(tmp_path):
def test_parse_file_path_missing(tmp_path: Path):
"""Should system exit if a file is missing."""
with pytest.raises(SystemExit):
_parse_file_path(str(tmp_path / "missing.py"))
def test_parse_file_exit_on_dir(tmp_path):
def test_parse_file_exit_on_dir(tmp_path: Path):
"""Should system exit if a directory is passed"""
dir_path = tmp_path / "dir"
dir_path.mkdir()
@ -68,17 +69,17 @@ def test_build_uv_command_adds_editable_and_packages():
]
def test_get_npx_unix_like(monkeypatch):
def test_get_npx_unix_like(monkeypatch: pytest.MonkeyPatch):
"""Should return "npx" on unix-like systems."""
monkeypatch.setattr(sys, "platform", "linux")
assert _get_npx_command() == "npx"
def test_get_npx_windows(monkeypatch):
def test_get_npx_windows(monkeypatch: pytest.MonkeyPatch):
"""Should return one of the npx candidates on Windows."""
candidates = ["npx.cmd", "npx.exe", "npx"]
def fake_run(cmd, **kw):
def fake_run(cmd: list[str], **kw: Any) -> subprocess.CompletedProcess[bytes]:
if cmd[0] in candidates:
return subprocess.CompletedProcess(cmd, 0)
else:
@ -89,11 +90,11 @@ def test_get_npx_windows(monkeypatch):
assert _get_npx_command() in candidates
def test_get_npx_returns_none_when_npx_missing(monkeypatch):
def test_get_npx_returns_none_when_npx_missing(monkeypatch: pytest.MonkeyPatch):
"""Should give None if every candidate fails."""
monkeypatch.setattr(sys, "platform", "win32", raising=False)
def always_fail(*args, **kwargs):
def always_fail(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[bytes]:
raise subprocess.CalledProcessError(1, args[0])
monkeypatch.setattr(subprocess, "run", always_fail)

View file

@ -1,22 +1,22 @@
from collections.abc import Callable, Generator
from contextlib import asynccontextmanager
from typing import Any
from unittest.mock import patch
import pytest
from anyio.streams.memory import MemoryObjectSendStream
import mcp.shared.memory
from mcp.shared.message import SessionMessage
from mcp.types import (
JSONRPCNotification,
JSONRPCRequest,
)
from mcp.types import JSONRPCNotification, JSONRPCRequest
class SpyMemoryObjectSendStream:
def __init__(self, original_stream):
def __init__(self, original_stream: MemoryObjectSendStream[SessionMessage]):
self.original_stream = original_stream
self.sent_messages: list[SessionMessage] = []
async def send(self, message):
async def send(self, message: SessionMessage):
self.sent_messages.append(message)
await self.original_stream.send(message)
@ -26,16 +26,12 @@ class SpyMemoryObjectSendStream:
async def __aenter__(self):
return self
async def __aexit__(self, *args):
async def __aexit__(self, *args: Any):
await self.aclose()
class StreamSpyCollection:
def __init__(
self,
client_spy: SpyMemoryObjectSendStream,
server_spy: SpyMemoryObjectSendStream,
):
def __init__(self, client_spy: SpyMemoryObjectSendStream, server_spy: SpyMemoryObjectSendStream):
self.client = client_spy
self.server = server_spy
@ -80,7 +76,7 @@ class StreamSpyCollection:
@pytest.fixture
def stream_spy():
def stream_spy() -> Generator[Callable[[], StreamSpyCollection], None, None]:
"""Fixture that provides spies for both client and server write streams.
Example usage:
@ -103,7 +99,7 @@ def stream_spy():
server_spy = None
# Store references to our spy objects
def capture_spies(c_spy, s_spy):
def capture_spies(c_spy: SpyMemoryObjectSendStream, s_spy: SpyMemoryObjectSendStream):
nonlocal client_spy, server_spy
client_spy = c_spy
server_spy = s_spy

View file

@ -11,12 +11,7 @@ from inline_snapshot import Is, snapshot
from pydantic import AnyHttpUrl, AnyUrl
from mcp.client.auth import OAuthClientProvider, PKCEParameters
from mcp.shared.auth import (
OAuthClientInformationFull,
OAuthClientMetadata,
OAuthToken,
ProtectedResourceMetadata,
)
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken, ProtectedResourceMetadata
class MockTokenStorage:
@ -66,7 +61,7 @@ def valid_tokens():
@pytest.fixture
def oauth_provider(client_metadata, mock_storage):
def oauth_provider(client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage):
async def redirect_handler(url: str) -> None:
"""Mock redirect handler."""
pass
@ -115,7 +110,9 @@ class TestOAuthContext:
"""Test OAuth context functionality."""
@pytest.mark.anyio
async def test_oauth_provider_initialization(self, oauth_provider, client_metadata, mock_storage):
async def test_oauth_provider_initialization(
self, oauth_provider: OAuthClientProvider, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage
):
"""Test OAuthClientProvider basic setup."""
assert oauth_provider.context.server_url == "https://api.example.com/v1/mcp"
assert oauth_provider.context.client_metadata == client_metadata
@ -123,7 +120,7 @@ class TestOAuthContext:
assert oauth_provider.context.timeout == 300.0
assert oauth_provider.context is not None
def test_context_url_parsing(self, oauth_provider):
def test_context_url_parsing(self, oauth_provider: OAuthClientProvider):
"""Test get_authorization_base_url() extracts base URLs correctly."""
context = oauth_provider.context
@ -145,7 +142,7 @@ class TestOAuthContext:
)
@pytest.mark.anyio
async def test_token_validity_checking(self, oauth_provider, mock_storage, valid_tokens):
async def test_token_validity_checking(self, oauth_provider: OAuthClientProvider, valid_tokens: OAuthToken):
"""Test is_token_valid() and can_refresh_token() logic."""
context = oauth_provider.context
@ -180,7 +177,7 @@ class TestOAuthContext:
context.client_info = None
assert not context.can_refresh_token()
def test_clear_tokens(self, oauth_provider, valid_tokens):
def test_clear_tokens(self, oauth_provider: OAuthClientProvider, valid_tokens: OAuthToken):
"""Test clear_tokens() removes token data."""
context = oauth_provider.context
context.current_tokens = valid_tokens
@ -198,7 +195,9 @@ class TestOAuthFlow:
"""Test OAuth flow methods."""
@pytest.mark.anyio
async def test_discover_protected_resource_request(self, client_metadata, mock_storage):
async def test_discover_protected_resource_request(
self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage
):
"""Test protected resource discovery request building maintains backward compatibility."""
async def redirect_handler(url: str) -> None:
@ -236,7 +235,7 @@ class TestOAuthFlow:
assert "mcp-protocol-version" in request.headers
@pytest.mark.anyio
def test_create_oauth_metadata_request(self, oauth_provider):
def test_create_oauth_metadata_request(self, oauth_provider: OAuthClientProvider):
"""Test OAuth metadata discovery request building."""
request = oauth_provider._create_oauth_metadata_request("https://example.com")
@ -250,7 +249,7 @@ class TestOAuthFallback:
"""Test OAuth discovery fallback behavior for legacy (act as AS not RS) servers."""
@pytest.mark.anyio
async def test_oauth_discovery_fallback_order(self, oauth_provider):
async def test_oauth_discovery_fallback_order(self, oauth_provider: OAuthClientProvider):
"""Test fallback URL construction order."""
discovery_urls = oauth_provider._get_discovery_urls()
@ -262,7 +261,7 @@ class TestOAuthFallback:
]
@pytest.mark.anyio
async def test_oauth_discovery_fallback_conditions(self, oauth_provider):
async def test_oauth_discovery_fallback_conditions(self, oauth_provider: OAuthClientProvider):
"""Test the conditions during which an AS metadata discovery fallback will be attempted."""
# Ensure no tokens are stored
oauth_provider.context.current_tokens = None
@ -365,7 +364,7 @@ class TestOAuthFallback:
token_request = await auth_flow.asend(token_response)
@pytest.mark.anyio
async def test_handle_metadata_response_success(self, oauth_provider):
async def test_handle_metadata_response_success(self, oauth_provider: OAuthClientProvider):
"""Test successful metadata response handling."""
# Create minimal valid OAuth metadata
content = b"""{
@ -381,7 +380,7 @@ class TestOAuthFallback:
assert str(oauth_provider.context.oauth_metadata.issuer) == "https://auth.example.com/"
@pytest.mark.anyio
async def test_register_client_request(self, oauth_provider):
async def test_register_client_request(self, oauth_provider: OAuthClientProvider):
"""Test client registration request building."""
request = await oauth_provider._register_client()
@ -391,7 +390,7 @@ class TestOAuthFallback:
assert request.headers["Content-Type"] == "application/json"
@pytest.mark.anyio
async def test_register_client_skip_if_registered(self, oauth_provider, mock_storage):
async def test_register_client_skip_if_registered(self, oauth_provider: OAuthClientProvider):
"""Test client registration is skipped if already registered."""
# Set existing client info
client_info = OAuthClientInformationFull(
@ -405,7 +404,7 @@ class TestOAuthFallback:
assert request is None
@pytest.mark.anyio
async def test_token_exchange_request(self, oauth_provider):
async def test_token_exchange_request(self, oauth_provider: OAuthClientProvider):
"""Test token exchange request building."""
# Set up required context
oauth_provider.context.client_info = OAuthClientInformationFull(
@ -429,7 +428,7 @@ class TestOAuthFallback:
assert "client_secret=test_secret" in content
@pytest.mark.anyio
async def test_refresh_token_request(self, oauth_provider, valid_tokens):
async def test_refresh_token_request(self, oauth_provider: OAuthClientProvider, valid_tokens: OAuthToken):
"""Test refresh token request building."""
# Set up required context
oauth_provider.context.current_tokens = valid_tokens
@ -538,11 +537,11 @@ class TestRegistrationResponse:
"""Test client registration response handling."""
@pytest.mark.anyio
async def test_handle_registration_response_reads_before_accessing_text(self, oauth_provider):
async def test_handle_registration_response_reads_before_accessing_text(self, oauth_provider: OAuthClientProvider):
"""Test that response.aread() is called before accessing response.text."""
# Track if aread() was called
class MockResponse:
class MockResponse(httpx.Response):
def __init__(self):
self.status_code = 400
self._aread_called = False
@ -574,7 +573,9 @@ class TestAuthFlow:
"""Test the auth flow in httpx."""
@pytest.mark.anyio
async def test_auth_flow_with_valid_tokens(self, oauth_provider, mock_storage, valid_tokens):
async def test_auth_flow_with_valid_tokens(
self, oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage, valid_tokens: OAuthToken
):
"""Test auth flow when tokens are already valid."""
# Pre-store valid tokens
await mock_storage.set_tokens(valid_tokens)
@ -600,7 +601,7 @@ class TestAuthFlow:
pass # Expected
@pytest.mark.anyio
async def test_auth_flow_with_no_tokens(self, oauth_provider, mock_storage):
async def test_auth_flow_with_no_tokens(self, oauth_provider: OAuthClientProvider):
"""Test auth flow when no tokens are available, triggering the full OAuth flow."""
# Ensure no tokens are stored
oauth_provider.context.current_tokens = None
@ -810,7 +811,11 @@ class TestProtectedResourceWWWAuthenticate:
],
)
def test_extract_resource_metadata_from_www_auth_valid_cases(
self, client_metadata, mock_storage, www_auth_header, expected_url
self,
client_metadata: OAuthClientMetadata,
mock_storage: MockTokenStorage,
www_auth_header: str,
expected_url: str,
):
"""Test extraction of resource_metadata URL from various valid WWW-Authenticate headers."""
@ -862,7 +867,12 @@ class TestProtectedResourceWWWAuthenticate:
],
)
def test_extract_resource_metadata_from_www_auth_invalid_cases(
self, client_metadata, mock_storage, status_code, www_auth_header, description
self,
client_metadata: OAuthClientMetadata,
mock_storage: MockTokenStorage,
status_code: int,
www_auth_header: str | None,
description: str,
):
"""Test extraction returns None for invalid cases."""

View file

@ -1,15 +1,16 @@
from collections.abc import Callable
import pytest
from mcp.server.fastmcp import FastMCP
from mcp.shared.memory import (
create_connected_server_and_client_session as create_session,
)
from mcp.shared.memory import create_connected_server_and_client_session as create_session
from .conftest import StreamSpyCollection
# Mark the whole module for async tests
pytestmark = pytest.mark.anyio
async def test_list_tools_cursor_parameter(stream_spy):
async def test_list_tools_cursor_parameter(stream_spy: Callable[[], StreamSpyCollection]):
"""Test that the cursor parameter is accepted for list_tools
and that it is correctly passed to the server.
@ -64,7 +65,7 @@ async def test_list_tools_cursor_parameter(stream_spy):
assert list_tools_requests[0].params["cursor"] == ""
async def test_list_resources_cursor_parameter(stream_spy):
async def test_list_resources_cursor_parameter(stream_spy: Callable[[], StreamSpyCollection]):
"""Test that the cursor parameter is accepted for list_resources
and that it is correctly passed to the server.
@ -114,7 +115,7 @@ async def test_list_resources_cursor_parameter(stream_spy):
assert list_resources_requests[0].params["cursor"] == ""
async def test_list_prompts_cursor_parameter(stream_spy):
async def test_list_prompts_cursor_parameter(stream_spy: Callable[[], StreamSpyCollection]):
"""Test that the cursor parameter is accepted for list_prompts
and that it is correctly passed to the server.
See: https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination#request-format
@ -163,7 +164,7 @@ async def test_list_prompts_cursor_parameter(stream_spy):
assert list_prompts_requests[0].params["cursor"] == ""
async def test_list_resource_templates_cursor_parameter(stream_spy):
async def test_list_resource_templates_cursor_parameter(stream_spy: Callable[[], StreamSpyCollection]):
"""Test that the cursor parameter is accepted for list_resource_templates
and that it is correctly passed to the server.

View file

@ -1,5 +1,6 @@
import logging
from contextlib import contextmanager
from typing import Any
from unittest.mock import patch
import pytest
@ -53,7 +54,7 @@ class TestClientOutputSchemaValidation:
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
async def call_tool(name: str, arguments: dict[str, Any]):
# Return invalid structured content - age is string instead of integer
# The low-level server will wrap this in CallToolResult
return {"name": "John", "age": "invalid"} # Invalid: age should be int
@ -92,7 +93,7 @@ class TestClientOutputSchemaValidation:
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
async def call_tool(name: str, arguments: dict[str, Any]):
# Return invalid structured content - result is string instead of integer
return {"result": "not_a_number"} # Invalid: should be int
@ -123,7 +124,7 @@ class TestClientOutputSchemaValidation:
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
async def call_tool(name: str, arguments: dict[str, Any]):
# Return invalid structured content - values should be integers
return {"alice": "100", "bob": "85"} # Invalid: values should be int
@ -158,7 +159,7 @@ class TestClientOutputSchemaValidation:
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
async def call_tool(name: str, arguments: dict[str, Any]):
# Return structured content missing required field 'email'
return {"name": "John", "age": 30} # Missing required 'email'
@ -170,17 +171,17 @@ class TestClientOutputSchemaValidation:
assert "Invalid structured content returned by tool get_person" in str(exc_info.value)
@pytest.mark.anyio
async def test_tool_not_listed_warning(self, caplog):
async def test_tool_not_listed_warning(self, caplog: pytest.LogCaptureFixture):
"""Test that client logs warning when tool is not in list_tools but has outputSchema"""
server = Server("test-server")
@server.list_tools()
async def list_tools():
async def list_tools() -> list[Tool]:
# Return empty list - tool is not listed
return []
@server.call_tool()
async def call_tool(name: str, arguments: dict):
async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
# Server still responds to the tool call with structured content
return {"result": 42}

View file

@ -1,14 +1,12 @@
from typing import Any
from unittest.mock import patch
import anyio
import pytest
from mcp.shared.session import BaseSession
from mcp.types import (
ClientRequest,
EmptyResult,
PingRequest,
)
from mcp.shared.message import SessionMessage
from mcp.shared.session import BaseSession, RequestId, SendResultT
from mcp.types import ClientNotification, ClientRequest, ClientResult, EmptyResult, ErrorData, PingRequest
@pytest.mark.anyio
@ -20,13 +18,13 @@ async def test_send_request_stream_cleanup():
"""
# Create a mock session with the minimal required functionality
class TestSession(BaseSession):
async def _send_response(self, request_id, response):
class TestSession(BaseSession[ClientRequest, ClientNotification, ClientResult, Any, Any]):
async def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None:
pass
# Create streams
write_stream_send, write_stream_receive = anyio.create_memory_object_stream(1)
read_stream_send, read_stream_receive = anyio.create_memory_object_stream(1)
write_stream_send, write_stream_receive = anyio.create_memory_object_stream[SessionMessage](1)
read_stream_send, read_stream_receive = anyio.create_memory_object_stream[SessionMessage](1)
# Create the session
session = TestSession(
@ -37,14 +35,10 @@ async def test_send_request_stream_cleanup():
)
# Create a test request
request = ClientRequest(
PingRequest(
method="ping",
)
)
request = ClientRequest(PingRequest(method="ping"))
# Patch the _write_stream.send method to raise an exception
async def mock_send(*args, **kwargs):
async def mock_send(*args: Any, **kwargs: Any):
raise RuntimeError("Simulated network error")
# Record the response streams before the test

View file

@ -5,11 +5,7 @@ import pytest
import mcp
from mcp import types
from mcp.client.session_group import (
ClientSessionGroup,
SseServerParameters,
StreamableHttpParameters,
)
from mcp.client.session_group import ClientSessionGroup, SseServerParameters, StreamableHttpParameters
from mcp.client.stdio import StdioServerParameters
from mcp.shared.exceptions import McpError
@ -54,7 +50,7 @@ class TestClientSessionGroup:
mock_session = mock.AsyncMock()
# --- Prepare Session Group ---
def hook(name, server_info):
def hook(name: str, server_info: types.Implementation) -> str:
return f"{(server_info.name)}-{name}"
mcp_session_group = ClientSessionGroup(component_name_hook=hook)
@ -79,7 +75,7 @@ class TestClientSessionGroup:
{"name": "value1", "args": {}},
)
async def test_connect_to_server(self, mock_exit_stack):
async def test_connect_to_server(self, mock_exit_stack: contextlib.AsyncExitStack):
"""Test connecting to a server and aggregating components."""
# --- Mock Dependencies ---
mock_server_info = mock.Mock(spec=types.Implementation)
@ -116,7 +112,7 @@ class TestClientSessionGroup:
mock_session.list_resources.assert_awaited_once()
mock_session.list_prompts.assert_awaited_once()
async def test_connect_to_server_with_name_hook(self, mock_exit_stack):
async def test_connect_to_server_with_name_hook(self, mock_exit_stack: contextlib.AsyncExitStack):
"""Test connecting with a component name hook."""
# --- Mock Dependencies ---
mock_server_info = mock.Mock(spec=types.Implementation)
@ -208,7 +204,7 @@ class TestClientSessionGroup:
assert "res1" not in group._resources
assert "prm1" not in group._prompts
async def test_connect_to_server_duplicate_tool_raises_error(self, mock_exit_stack):
async def test_connect_to_server_duplicate_tool_raises_error(self, mock_exit_stack: contextlib.AsyncExitStack):
"""Test McpError raised when connecting a server with a dup name."""
# --- Setup Pre-existing State ---
group = ClientSessionGroup(exit_stack=mock_exit_stack)
@ -282,9 +278,9 @@ class TestClientSessionGroup:
)
async def test_establish_session_parameterized(
self,
server_params_instance,
client_type_name, # Just for clarity or conditional logic if needed
patch_target_for_client_func,
server_params_instance: StdioServerParameters | SseServerParameters | StreamableHttpParameters,
client_type_name: str, # Just for clarity or conditional logic if needed
patch_target_for_client_func: str,
):
with mock.patch("mcp.client.session_group.mcp.ClientSession") as mock_ClientSession_class:
with mock.patch(patch_target_for_client_func) as mock_specific_client_func:
@ -338,8 +334,10 @@ class TestClientSessionGroup:
# --- Assertions ---
# 1. Assert the correct specific client function was called
if client_type_name == "stdio":
assert isinstance(server_params_instance, StdioServerParameters)
mock_specific_client_func.assert_called_once_with(server_params_instance)
elif client_type_name == "sse":
assert isinstance(server_params_instance, SseServerParameters)
mock_specific_client_func.assert_called_once_with(
url=server_params_instance.url,
headers=server_params_instance.headers,
@ -347,6 +345,7 @@ class TestClientSessionGroup:
sse_read_timeout=server_params_instance.sse_read_timeout,
)
elif client_type_name == "streamablehttp":
assert isinstance(server_params_instance, StreamableHttpParameters)
mock_specific_client_func.assert_called_once_with(
url=server_params_instance.url,
headers=server_params_instance.headers,

View file

@ -9,28 +9,25 @@ import anyio
import pytest
from mcp.client.session import ClientSession
from mcp.client.stdio import (
StdioServerParameters,
_create_platform_compatible_process,
stdio_client,
)
from mcp.client.stdio import StdioServerParameters, _create_platform_compatible_process, stdio_client
from mcp.shared.exceptions import McpError
from mcp.shared.message import SessionMessage
from mcp.types import CONNECTION_CLOSED, JSONRPCMessage, JSONRPCRequest, JSONRPCResponse
from tests.shared.test_win32_utils import escape_path_for_python
from ..shared.test_win32_utils import escape_path_for_python
# Timeout for cleanup of processes that ignore SIGTERM
# This timeout ensures the test fails quickly if the cleanup logic doesn't have
# proper fallback mechanisms (SIGINT/SIGKILL) for processes that ignore SIGTERM
SIGTERM_IGNORING_PROCESS_TIMEOUT = 5.0
tee: str = shutil.which("tee") # type: ignore
python: str = shutil.which("python") # type: ignore
tee = shutil.which("tee")
@pytest.mark.anyio
@pytest.mark.skipif(tee is None, reason="could not find tee command")
async def test_stdio_context_manager_exiting():
assert tee is not None
async with stdio_client(StdioServerParameters(command=tee)) as (_, _):
pass
@ -38,6 +35,7 @@ async def test_stdio_context_manager_exiting():
@pytest.mark.anyio
@pytest.mark.skipif(tee is None, reason="could not find tee command")
async def test_stdio_client():
assert tee is not None
server_parameters = StdioServerParameters(command=tee)
async with stdio_client(server_parameters) as (read_stream, write_stream):
@ -52,7 +50,7 @@ async def test_stdio_client():
session_message = SessionMessage(message)
await write_stream.send(session_message)
read_messages = []
read_messages: list[JSONRPCMessage] = []
async with read_stream:
async for message in read_stream:
if isinstance(message, Exception):
@ -118,7 +116,7 @@ async def test_stdio_client_universal_cleanup():
"""
import time
import sys
# Simulate a long-running process
for i in range(100):
time.sleep(0.1)
@ -136,7 +134,7 @@ async def test_stdio_client_universal_cleanup():
start_time = time.time()
with anyio.move_on_after(8.0) as cancel_scope:
async with stdio_client(server_params) as (read_stream, write_stream):
async with stdio_client(server_params) as (_, _):
# Immediately exit - this triggers cleanup while process is still running
pass
@ -195,7 +193,7 @@ async def test_stdio_client_sigint_only_process():
try:
# Use anyio timeout to prevent test from hanging forever
with anyio.move_on_after(5.0) as cancel_scope:
async with stdio_client(server_params) as (read_stream, write_stream):
async with stdio_client(server_params) as (_, _):
# Let the process start and begin ignoring SIGTERM
await anyio.sleep(0.5)
# Exit context triggers cleanup - this should not hang
@ -532,7 +530,7 @@ async def test_stdio_client_graceful_stdin_exit():
script_content = textwrap.dedent(
"""
import sys
# Read from stdin until it's closed
try:
while True:
@ -541,7 +539,7 @@ async def test_stdio_client_graceful_stdin_exit():
break
except:
pass
# Exit gracefully
sys.exit(0)
"""
@ -556,7 +554,7 @@ async def test_stdio_client_graceful_stdin_exit():
# Use anyio timeout to prevent test from hanging forever
with anyio.move_on_after(5.0) as cancel_scope:
async with stdio_client(server_params) as (read_stream, write_stream):
async with stdio_client(server_params) as (_, _):
# Let the process start and begin reading stdin
await anyio.sleep(0.2)
# Exit context triggers cleanup - process should exit from stdin closure
@ -590,16 +588,16 @@ async def test_stdio_client_stdin_close_ignored():
import signal
import sys
import time
# Set up SIGTERM handler to exit cleanly
def sigterm_handler(signum, frame):
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler)
# Close stdin immediately to simulate ignoring it
sys.stdin.close()
# Keep running until SIGTERM
while True:
time.sleep(0.1)
@ -615,7 +613,7 @@ async def test_stdio_client_stdin_close_ignored():
# Use anyio timeout to prevent test from hanging forever
with anyio.move_on_after(7.0) as cancel_scope:
async with stdio_client(server_params) as (read_stream, write_stream):
async with stdio_client(server_params) as (_, _):
# Let the process start
await anyio.sleep(0.2)
# Exit context triggers cleanup

View file

@ -12,13 +12,19 @@ import sys
import tempfile
import textwrap
from pathlib import Path
from typing import TYPE_CHECKING
import anyio
import pytest
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import _create_platform_compatible_process, stdio_client
from tests.shared.test_win32_utils import escape_path_for_python
# TODO(Marcelo): This doesn't seem to be the right path. We should fix this.
if TYPE_CHECKING:
from ..shared.test_win32_utils import escape_path_for_python
else:
from tests.shared.test_win32_utils import escape_path_for_python
@pytest.mark.anyio
@ -52,10 +58,10 @@ async def test_lifespan_cleanup_executed():
from pathlib import Path
from contextlib import asynccontextmanager
from mcp.server.fastmcp import FastMCP
STARTUP_MARKER = {escape_path_for_python(startup_marker)}
CLEANUP_MARKER = {escape_path_for_python(cleanup_marker)}
@asynccontextmanager
async def lifespan(server):
# Write startup marker
@ -65,13 +71,13 @@ async def test_lifespan_cleanup_executed():
finally:
# This cleanup code now runs properly during shutdown
Path(CLEANUP_MARKER).write_text("cleaned up")
mcp = FastMCP("test-server", lifespan=lifespan)
@mcp.tool()
def echo(text: str) -> str:
return text
if __name__ == "__main__":
mcp.run()
""")
@ -160,10 +166,10 @@ async def test_stdin_close_triggers_cleanup():
from pathlib import Path
from contextlib import asynccontextmanager
from mcp.server.fastmcp import FastMCP
STARTUP_MARKER = {escape_path_for_python(startup_marker)}
CLEANUP_MARKER = {escape_path_for_python(cleanup_marker)}
@asynccontextmanager
async def lifespan(server):
# Write startup marker
@ -173,13 +179,13 @@ async def test_stdin_close_triggers_cleanup():
finally:
# This cleanup code runs when stdin closes, enabling graceful shutdown
Path(CLEANUP_MARKER).write_text("cleaned up")
mcp = FastMCP("test-server", lifespan=lifespan)
@mcp.tool()
def echo(text: str) -> str:
return text
if __name__ == "__main__":
# The server should exit gracefully when stdin closes
try:

View file

@ -11,7 +11,7 @@ async def test_messages_are_executed_concurrently_tools():
server = FastMCP("test")
event = anyio.Event()
tool_started = anyio.Event()
call_order = []
call_order: list[str] = []
@server.tool("sleep")
async def sleep_tool():
@ -52,7 +52,7 @@ async def test_messages_are_executed_concurrently_tools_and_resources():
server = FastMCP("test")
event = anyio.Event()
tool_started = anyio.Event()
call_order = []
call_order: list[str] = []
@server.tool("sleep")
async def sleep_tool():

View file

@ -12,6 +12,7 @@ from mcp.types import (
JSONRPCMessage,
JSONRPCNotification,
JSONRPCRequest,
JSONRPCResponse,
NotificationParams,
)
@ -23,8 +24,8 @@ async def test_request_id_match() -> None:
custom_request_id = "test-123"
# Create memory streams for communication
client_writer, client_reader = anyio.create_memory_object_stream(1)
server_writer, server_reader = anyio.create_memory_object_stream(1)
client_writer, client_reader = anyio.create_memory_object_stream[SessionMessage | Exception](1)
server_writer, server_reader = anyio.create_memory_object_stream[SessionMessage | Exception](1)
# Server task to process the request
async def run_server():
@ -85,6 +86,9 @@ async def test_request_id_match() -> None:
response = await server_reader.receive()
# Verify response ID matches request ID
assert isinstance(response, SessionMessage)
assert isinstance(response.message, JSONRPCMessage)
assert isinstance(response.message.root, JSONRPCResponse)
assert response.message.root.id == custom_request_id, "Response ID should match request ID"
# Cancel server task

View file

@ -3,6 +3,7 @@ from contextlib import asynccontextmanager
from dataclasses import dataclass
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
class Database: # Replace with your actual DB type
@ -44,7 +45,7 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
# Access type-safe lifespan context in tools
@mcp.tool()
def query_db(ctx: Context) -> str:
def query_db(ctx: Context[ServerSession, AppContext]) -> str:
"""Tool that uses initialized resources"""
db = ctx.request_context.lifespan_context.db
return db.query()

View file

@ -3,15 +3,18 @@
from collections.abc import Sequence
from datetime import timedelta
from pathlib import Path
from typing import Any
import anyio
import pytest
from anyio.abc import TaskStatus
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from mcp import types
from mcp.client.session import ClientSession
from mcp.server.lowlevel import Server
from mcp.shared.exceptions import McpError
from mcp.shared.message import SessionMessage
from mcp.types import ContentBlock, TextContent
@ -46,7 +49,7 @@ async def test_notification_validation_error(tmp_path: Path):
]
@server.call_tool()
async def slow_tool(name: str, arg) -> Sequence[ContentBlock]:
async def slow_tool(name: str, arguments: dict[str, Any]) -> Sequence[ContentBlock]:
nonlocal request_count
request_count += 1
@ -58,8 +61,8 @@ async def test_notification_validation_error(tmp_path: Path):
return [TextContent(type="text", text=f"unknown {request_count}")]
async def server_handler(
read_stream,
write_stream,
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
write_stream: MemoryObjectSendStream[SessionMessage],
task_status: TaskStatus[str] = anyio.TASK_STATUS_IGNORED,
):
with anyio.CancelScope() as scope:
@ -71,7 +74,11 @@ async def test_notification_validation_error(tmp_path: Path):
raise_exceptions=True,
)
async def client(read_stream, write_stream, scope):
async def client(
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
write_stream: MemoryObjectSendStream[SessionMessage],
scope: anyio.CancelScope,
):
# Use a timeout that's:
# - Long enough for fast operations (>10ms)
# - Short enough for slow operations (<200ms)
@ -99,8 +106,8 @@ async def test_notification_validation_error(tmp_path: Path):
scope.cancel()
# Run server and client in separate task groups to avoid cancellation
server_writer, server_reader = anyio.create_memory_object_stream(1)
client_writer, client_reader = anyio.create_memory_object_stream(1)
server_writer, server_reader = anyio.create_memory_object_stream[SessionMessage](1)
client_writer, client_reader = anyio.create_memory_object_stream[SessionMessage](1)
async with anyio.create_task_group() as tg:
scope = await tg.start(server_handler, server_reader, client_writer)

View file

@ -1,6 +1,8 @@
# Claude Debug
"""Test for HackerOne vulnerability report #3156202 - malformed input DOS."""
from typing import Any
import anyio
import pytest
@ -118,7 +120,7 @@ async def test_multiple_concurrent_malformed_requests():
),
):
# Send multiple malformed requests concurrently
malformed_requests = []
malformed_requests: list[SessionMessage] = []
for i in range(10):
malformed_request = JSONRPCRequest(
jsonrpc="2.0",
@ -137,7 +139,7 @@ async def test_multiple_concurrent_malformed_requests():
await anyio.sleep(0.2)
# Verify we get error responses for all requests
error_responses = []
error_responses: list[Any] = []
try:
while True:
response_message = write_receive_stream.receive_nowait()

View file

@ -11,16 +11,8 @@ from starlette.datastructures import Headers
from starlette.requests import Request
from starlette.types import Message, Receive, Scope, Send
from mcp.server.auth.middleware.bearer_auth import (
AuthenticatedUser,
BearerAuthBackend,
RequireAuthMiddleware,
)
from mcp.server.auth.provider import (
AccessToken,
OAuthAuthorizationServerProvider,
ProviderTokenVerifier,
)
from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser, BearerAuthBackend, RequireAuthMiddleware
from mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvider, ProviderTokenVerifier
class MockOAuthProvider:
@ -31,7 +23,7 @@ class MockOAuthProvider:
"""
def __init__(self):
self.tokens = {} # token -> AccessToken
self.tokens: dict[str, AccessToken] = {} # token -> AccessToken
def add_token(self, token: str, access_token: AccessToken) -> None:
"""Add a token to the provider."""
@ -287,7 +279,7 @@ class TestRequireAuthMiddleware:
async def receive() -> Message:
return {"type": "http.request"}
sent_messages = []
sent_messages: list[Message] = []
async def send(message: Message) -> None:
sent_messages.append(message)
@ -311,7 +303,7 @@ class TestRequireAuthMiddleware:
async def receive() -> Message:
return {"type": "http.request"}
sent_messages = []
sent_messages: list[Message] = []
async def send(message: Message) -> None:
sent_messages.append(message)
@ -340,7 +332,7 @@ class TestRequireAuthMiddleware:
async def receive() -> Message:
return {"type": "http.request"}
sent_messages = []
sent_messages: list[Message] = []
async def send(message: Message) -> None:
sent_messages.append(message)
@ -368,7 +360,7 @@ class TestRequireAuthMiddleware:
async def receive() -> Message:
return {"type": "http.request"}
sent_messages = []
sent_messages: list[Message] = []
async def send(message: Message) -> None:
sent_messages.append(message)

View file

@ -3,6 +3,7 @@ Tests for OAuth error handling in the auth handlers.
"""
import unittest.mock
from typing import TYPE_CHECKING, Any
from urllib.parse import parse_qs, urlparse
import httpx
@ -11,15 +12,14 @@ from httpx import ASGITransport
from pydantic import AnyHttpUrl
from starlette.applications import Starlette
from mcp.server.auth.provider import (
AuthorizeError,
RegistrationError,
TokenError,
)
from mcp.server.auth.provider import AuthorizeError, RegistrationError, TokenError
from mcp.server.auth.routes import create_auth_routes
from tests.server.fastmcp.auth.test_auth_integration import (
MockOAuthProvider,
)
# TODO(Marcelo): This TYPE_CHECKING shouldn't be here, but pytest doesn't seem to get the module correctly.
if TYPE_CHECKING:
from ...server.fastmcp.auth.test_auth_integration import MockOAuthProvider
else:
from tests.server.fastmcp.auth.test_auth_integration import MockOAuthProvider
@pytest.fixture
@ -29,7 +29,7 @@ def oauth_provider():
@pytest.fixture
def app(oauth_provider):
def app(oauth_provider: MockOAuthProvider):
from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions
# Enable client registration
@ -49,7 +49,7 @@ def app(oauth_provider):
@pytest.fixture
def client(app):
def client(app: Starlette):
transport = ASGITransport(app=app)
# Use base_url without a path since routes are directly on the app
return httpx.AsyncClient(transport=transport, base_url="http://localhost")
@ -74,7 +74,7 @@ def pkce_challenge():
@pytest.fixture
async def registered_client(client):
async def registered_client(client: httpx.AsyncClient) -> dict[str, Any]:
"""Create and register a test client."""
# Default client metadata
client_metadata = {
@ -94,7 +94,7 @@ async def registered_client(client):
class TestRegistrationErrorHandling:
@pytest.mark.anyio
async def test_registration_error_handling(self, client, oauth_provider):
async def test_registration_error_handling(self, client: httpx.AsyncClient, oauth_provider: MockOAuthProvider):
# Mock the register_client method to raise a registration error
with unittest.mock.patch.object(
oauth_provider,
@ -128,7 +128,13 @@ class TestRegistrationErrorHandling:
class TestAuthorizeErrorHandling:
@pytest.mark.anyio
async def test_authorize_error_handling(self, client, oauth_provider, registered_client, pkce_challenge):
async def test_authorize_error_handling(
self,
client: httpx.AsyncClient,
oauth_provider: MockOAuthProvider,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],
):
# Mock the authorize method to raise an authorize error
with unittest.mock.patch.object(
oauth_provider,
@ -165,7 +171,13 @@ class TestAuthorizeErrorHandling:
class TestTokenErrorHandling:
@pytest.mark.anyio
async def test_token_error_handling_auth_code(self, client, oauth_provider, registered_client, pkce_challenge):
async def test_token_error_handling_auth_code(
self,
client: httpx.AsyncClient,
oauth_provider: MockOAuthProvider,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],
):
# Register the client and get an auth code
client_id = registered_client["client_id"]
client_secret = registered_client["client_secret"]
@ -218,7 +230,13 @@ class TestTokenErrorHandling:
assert data["error_description"] == "The authorization code is invalid"
@pytest.mark.anyio
async def test_token_error_handling_refresh_token(self, client, oauth_provider, registered_client, pkce_challenge):
async def test_token_error_handling_refresh_token(
self,
client: httpx.AsyncClient,
oauth_provider: MockOAuthProvider,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],
):
# Register the client and get tokens
client_id = registered_client["client_id"]
client_secret = registered_client["client_secret"]

View file

@ -7,6 +7,7 @@ import hashlib
import secrets
import time
import unittest.mock
from typing import Any
from urllib.parse import parse_qs, urlparse
import httpx
@ -22,24 +23,17 @@ from mcp.server.auth.provider import (
RefreshToken,
construct_redirect_uri,
)
from mcp.server.auth.routes import (
ClientRegistrationOptions,
RevocationOptions,
create_auth_routes,
)
from mcp.shared.auth import (
OAuthClientInformationFull,
OAuthToken,
)
from mcp.server.auth.routes import ClientRegistrationOptions, RevocationOptions, create_auth_routes
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
# Mock OAuth provider for testing
class MockOAuthProvider(OAuthAuthorizationServerProvider):
class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
def __init__(self):
self.clients = {}
self.auth_codes = {} # code -> {client_id, code_challenge, redirect_uri}
self.tokens = {} # token -> {client_id, scopes, expires_at}
self.refresh_tokens = {} # refresh_token -> access_token
self.clients: dict[str, OAuthClientInformationFull] = {}
self.auth_codes: dict[str, AuthorizationCode] = {} # code -> {client_id, code_challenge, redirect_uri}
self.tokens: dict[str, AccessToken] = {} # token -> {client_id, scopes, expires_at}
self.refresh_tokens: dict[str, str] = {} # refresh_token -> access_token
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
return self.clients.get(client_id)
@ -196,7 +190,7 @@ def mock_oauth_provider():
@pytest.fixture
def auth_app(mock_oauth_provider):
def auth_app(mock_oauth_provider: MockOAuthProvider):
# Create auth router
auth_routes = create_auth_routes(
mock_oauth_provider,
@ -217,13 +211,15 @@ def auth_app(mock_oauth_provider):
@pytest.fixture
async def test_client(auth_app):
async def test_client(auth_app: Starlette):
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=auth_app), base_url="https://mcptest.com") as client:
yield client
@pytest.fixture
async def registered_client(test_client: httpx.AsyncClient, request):
async def registered_client(
test_client: httpx.AsyncClient, request: pytest.FixtureRequest
) -> OAuthClientInformationFull:
"""Create and register a test client.
Parameters can be customized via indirect parameterization:
@ -259,7 +255,12 @@ def pkce_challenge():
@pytest.fixture
async def auth_code(test_client, registered_client, pkce_challenge, request):
async def auth_code(
test_client: httpx.AsyncClient,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],
request: pytest.FixtureRequest,
):
"""Get an authorization code.
Parameters can be customized via indirect parameterization:
@ -300,7 +301,13 @@ async def auth_code(test_client, registered_client, pkce_challenge, request):
@pytest.fixture
async def tokens(test_client, registered_client, auth_code, pkce_challenge, request):
async def tokens(
test_client: httpx.AsyncClient,
registered_client: dict[str, Any],
auth_code: dict[str, str],
pkce_challenge: dict[str, str],
request: pytest.FixtureRequest,
):
"""Exchange authorization code for tokens.
Parameters can be customized via indirect parameterization:
@ -373,7 +380,12 @@ class TestAuthEndpoints:
assert "error_description" in error_response # Contains validation error messages
@pytest.mark.anyio
async def test_token_invalid_auth_code(self, test_client, registered_client, pkce_challenge):
async def test_token_invalid_auth_code(
self,
test_client: httpx.AsyncClient,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],
):
"""Test token endpoint error - authorization code does not exist."""
# Try to use a non-existent authorization code
response = await test_client.post(
@ -398,11 +410,11 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_token_expired_auth_code(
self,
test_client,
registered_client,
auth_code,
pkce_challenge,
mock_oauth_provider,
test_client: httpx.AsyncClient,
registered_client: dict[str, Any],
auth_code: dict[str, str],
pkce_challenge: dict[str, str],
mock_oauth_provider: MockOAuthProvider,
):
"""Test token endpoint error - authorization code has expired."""
# Get the current time for our time mocking
@ -451,7 +463,13 @@ class TestAuthEndpoints:
],
indirect=True,
)
async def test_token_redirect_uri_mismatch(self, test_client, registered_client, auth_code, pkce_challenge):
async def test_token_redirect_uri_mismatch(
self,
test_client: httpx.AsyncClient,
registered_client: dict[str, Any],
auth_code: dict[str, str],
pkce_challenge: dict[str, str],
):
"""Test token endpoint error - redirect URI mismatch."""
# Try to use the code with a different redirect URI
response = await test_client.post(
@ -472,7 +490,9 @@ class TestAuthEndpoints:
assert "redirect_uri did not match" in error_response["error_description"]
@pytest.mark.anyio
async def test_token_code_verifier_mismatch(self, test_client, registered_client, auth_code):
async def test_token_code_verifier_mismatch(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], auth_code: dict[str, str]
):
"""Test token endpoint error - PKCE code verifier mismatch."""
# Try to use the code with an incorrect code verifier
response = await test_client.post(
@ -493,7 +513,7 @@ class TestAuthEndpoints:
assert "incorrect code_verifier" in error_response["error_description"]
@pytest.mark.anyio
async def test_token_invalid_refresh_token(self, test_client, registered_client):
async def test_token_invalid_refresh_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):
"""Test token endpoint error - refresh token does not exist."""
# Try to use a non-existent refresh token
response = await test_client.post(
@ -513,11 +533,10 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_token_expired_refresh_token(
self,
test_client,
registered_client,
auth_code,
pkce_challenge,
mock_oauth_provider,
test_client: httpx.AsyncClient,
registered_client: dict[str, Any],
auth_code: dict[str, str],
pkce_challenge: dict[str, str],
):
"""Test token endpoint error - refresh token has expired."""
# Step 1: First, let's create a token and refresh token at the current time
@ -560,7 +579,13 @@ class TestAuthEndpoints:
assert "refresh token has expired" in error_response["error_description"]
@pytest.mark.anyio
async def test_token_invalid_scope(self, test_client, registered_client, auth_code, pkce_challenge):
async def test_token_invalid_scope(
self,
test_client: httpx.AsyncClient,
registered_client: dict[str, Any],
auth_code: dict[str, str],
pkce_challenge: dict[str, str],
):
"""Test token endpoint error - invalid scope in refresh token request."""
# Exchange authorization code for tokens
token_response = await test_client.post(
@ -664,8 +689,9 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_client_registration_empty_redirect_uris(self, test_client: httpx.AsyncClient):
"""Test client registration with empty redirect_uris array."""
redirect_uris: list[str] = []
client_metadata = {
"redirect_uris": [], # Empty array
"redirect_uris": redirect_uris, # Empty array
"client_name": "Test Client",
}
@ -682,12 +708,7 @@ class TestAuthEndpoints:
)
@pytest.mark.anyio
async def test_authorize_form_post(
self,
test_client: httpx.AsyncClient,
mock_oauth_provider: MockOAuthProvider,
pkce_challenge,
):
async def test_authorize_form_post(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):
"""Test the authorization endpoint using POST with form-encoded data."""
# Register a client
client_metadata = {
@ -730,7 +751,7 @@ class TestAuthEndpoints:
self,
test_client: httpx.AsyncClient,
mock_oauth_provider: MockOAuthProvider,
pkce_challenge,
pkce_challenge: dict[str, str],
):
"""Test the full authorization flow."""
# 1. Register a client
@ -836,7 +857,7 @@ class TestAuthEndpoints:
assert await mock_oauth_provider.load_access_token(new_token_response["access_token"]) is None
@pytest.mark.anyio
async def test_revoke_invalid_token(self, test_client, registered_client):
async def test_revoke_invalid_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):
"""Test revoking an invalid token."""
response = await test_client.post(
"/revoke",
@ -850,7 +871,7 @@ class TestAuthEndpoints:
assert response.status_code == 200
@pytest.mark.anyio
async def test_revoke_with_malformed_token(self, test_client, registered_client):
async def test_revoke_with_malformed_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):
response = await test_client.post(
"/revoke",
data={
@ -874,10 +895,7 @@ class TestAuthEndpoints:
"scope": "read write profile admin", # 'admin' is not in valid_scopes
}
response = await test_client.post(
"/register",
json=client_metadata,
)
response = await test_client.post("/register", json=client_metadata)
assert response.status_code == 400
error_data = response.json()
assert "error" in error_data
@ -895,10 +913,7 @@ class TestAuthEndpoints:
# No scope specified
}
response = await test_client.post(
"/register",
json=client_metadata,
)
response = await test_client.post("/register", json=client_metadata)
assert response.status_code == 201
client_info = response.json()
@ -920,10 +935,7 @@ class TestAuthEndpoints:
"grant_types": ["authorization_code"],
}
response = await test_client.post(
"/register",
json=client_metadata,
)
response = await test_client.post("/register", json=client_metadata)
assert response.status_code == 400
error_data = response.json()
assert "error" in error_data
@ -935,7 +947,7 @@ class TestAuthorizeEndpointErrors:
"""Test error handling in the OAuth authorization endpoint."""
@pytest.mark.anyio
async def test_authorize_missing_client_id(self, test_client: httpx.AsyncClient, pkce_challenge):
async def test_authorize_missing_client_id(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):
"""Test authorization endpoint with missing client_id.
According to the OAuth2.0 spec, if client_id is missing, the server should
@ -959,7 +971,7 @@ class TestAuthorizeEndpointErrors:
assert "client_id" in response.text.lower()
@pytest.mark.anyio
async def test_authorize_invalid_client_id(self, test_client: httpx.AsyncClient, pkce_challenge):
async def test_authorize_invalid_client_id(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):
"""Test authorization endpoint with invalid client_id.
According to the OAuth2.0 spec, if client_id is invalid, the server should
@ -984,7 +996,7 @@ class TestAuthorizeEndpointErrors:
@pytest.mark.anyio
async def test_authorize_missing_redirect_uri(
self, test_client: httpx.AsyncClient, registered_client, pkce_challenge
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test authorization endpoint with missing redirect_uri.
@ -1010,7 +1022,7 @@ class TestAuthorizeEndpointErrors:
@pytest.mark.anyio
async def test_authorize_invalid_redirect_uri(
self, test_client: httpx.AsyncClient, registered_client, pkce_challenge
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test authorization endpoint with invalid redirect_uri.
@ -1050,7 +1062,7 @@ class TestAuthorizeEndpointErrors:
indirect=True,
)
async def test_authorize_missing_redirect_uri_multiple_registered(
self, test_client: httpx.AsyncClient, registered_client, pkce_challenge
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test endpoint with missing redirect_uri with multiple registered URIs.
@ -1076,7 +1088,7 @@ class TestAuthorizeEndpointErrors:
@pytest.mark.anyio
async def test_authorize_unsupported_response_type(
self, test_client: httpx.AsyncClient, registered_client, pkce_challenge
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test authorization endpoint with unsupported response_type.
@ -1110,7 +1122,7 @@ class TestAuthorizeEndpointErrors:
@pytest.mark.anyio
async def test_authorize_missing_response_type(
self, test_client: httpx.AsyncClient, registered_client, pkce_challenge
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test authorization endpoint with missing response_type.
@ -1142,7 +1154,9 @@ class TestAuthorizeEndpointErrors:
assert query_params["state"][0] == "test_state"
@pytest.mark.anyio
async def test_authorize_missing_pkce_challenge(self, test_client: httpx.AsyncClient, registered_client):
async def test_authorize_missing_pkce_challenge(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]
):
"""Test authorization endpoint with missing PKCE code_challenge.
Missing PKCE parameters should result in invalid_request error.
@ -1171,7 +1185,9 @@ class TestAuthorizeEndpointErrors:
assert query_params["state"][0] == "test_state"
@pytest.mark.anyio
async def test_authorize_invalid_scope(self, test_client: httpx.AsyncClient, registered_client, pkce_challenge):
async def test_authorize_invalid_scope(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test authorization endpoint with invalid scope.
Invalid scope should redirect with invalid_scope error.

View file

@ -1,13 +1,9 @@
from typing import Any
import pytest
from pydantic import FileUrl
from mcp.server.fastmcp.prompts.base import (
AssistantMessage,
Message,
Prompt,
TextContent,
UserMessage,
)
from mcp.server.fastmcp.prompts.base import AssistantMessage, Message, Prompt, TextContent, UserMessage
from mcp.types import EmbeddedResource, TextResourceContents
@ -65,7 +61,7 @@ class TestRenderPrompt:
@pytest.mark.anyio
async def test_fn_returns_multiple_messages(self):
expected = [
expected: list[Message] = [
UserMessage("Hello, world!"),
AssistantMessage("How can I help you today?"),
UserMessage("I'm looking for a restaurant in the center of town."),
@ -160,7 +156,7 @@ class TestRenderPrompt:
async def test_fn_returns_dict_with_resource(self):
"""Test returning a dict with resource content."""
async def fn() -> dict:
async def fn() -> dict[str, Any]:
return {
"role": "user",
"content": {

View file

@ -17,7 +17,7 @@ class TestPromptManager:
assert added == prompt
assert manager.get_prompt("fn") == prompt
def test_add_duplicate_prompt(self, caplog):
def test_add_duplicate_prompt(self, caplog: pytest.LogCaptureFixture):
"""Test adding the same prompt twice."""
def fn() -> str:
@ -30,7 +30,7 @@ class TestPromptManager:
assert first == second
assert "Prompt already exists" in caplog.text
def test_disable_warn_on_duplicate_prompts(self, caplog):
def test_disable_warn_on_duplicate_prompts(self, caplog: pytest.LogCaptureFixture):
"""Test disabling warning on duplicate prompts."""
def fn() -> str:

View file

@ -60,7 +60,7 @@ class TestFunctionResource:
async def test_json_conversion(self):
"""Test automatic JSON conversion of non-string results."""
def get_data() -> dict:
def get_data() -> dict[str, str]:
return {"key": "value"}
resource = FunctionResource(

Some files were not shown because too many files have changed in this diff Show more