Add pyright strict mode on the whole project (#1254)
This commit is contained in:
parent
ef4e167c1a
commit
c7671e470c
43 changed files with 652 additions and 674 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue