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
21 changed files with 652 additions and 674 deletions

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(

View file

@ -4,12 +4,7 @@ from tempfile import NamedTemporaryFile
import pytest
from pydantic import AnyUrl, FileUrl
from mcp.server.fastmcp.resources import (
FileResource,
FunctionResource,
ResourceManager,
ResourceTemplate,
)
from mcp.server.fastmcp.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate
@pytest.fixture
@ -57,7 +52,7 @@ class TestResourceManager:
assert first == second
assert manager.list_resources() == [resource]
def test_warn_on_duplicate_resources(self, temp_file: Path, caplog):
def test_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCaptureFixture):
"""Test warning on duplicate resources."""
manager = ResourceManager()
resource = FileResource(
@ -69,7 +64,7 @@ class TestResourceManager:
manager.add_resource(resource)
assert "Resource already exists" in caplog.text
def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog):
def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCaptureFixture):
"""Test disabling warning on duplicate resources."""
manager = ResourceManager(warn_on_duplicate_resources=False)
resource = FileResource(

View file

@ -1,4 +1,5 @@
import json
from typing import Any
import pytest
from pydantic import BaseModel
@ -12,7 +13,7 @@ class TestResourceTemplate:
def test_template_creation(self):
"""Test creating a template from a function."""
def my_func(key: str, value: int) -> dict:
def my_func(key: str, value: int) -> dict[str, Any]:
return {"key": key, "value": value}
template = ResourceTemplate.from_function(
@ -23,13 +24,12 @@ class TestResourceTemplate:
assert template.uri_template == "test://{key}/{value}"
assert template.name == "test"
assert template.mime_type == "text/plain" # default
test_input = {"key": "test", "value": 42}
assert template.fn(**test_input) == my_func(**test_input)
assert template.fn(key="test", value=42) == my_func(key="test", value=42)
def test_template_matches(self):
"""Test matching URIs against a template."""
def my_func(key: str, value: int) -> dict:
def my_func(key: str, value: int) -> dict[str, Any]:
return {"key": key, "value": value}
template = ResourceTemplate.from_function(
@ -50,7 +50,7 @@ class TestResourceTemplate:
async def test_create_resource(self):
"""Test creating a resource from a template."""
def my_func(key: str, value: int) -> dict:
def my_func(key: str, value: int) -> dict[str, Any]:
return {"key": key, "value": value}
template = ResourceTemplate.from_function(

View file

@ -7,7 +7,7 @@ from mcp.server.fastmcp import FastMCP
@pytest.fixture()
def test_dir(tmp_path_factory) -> Path:
def test_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
"""Create a temporary directory with test files."""
tmp = tmp_path_factory.mktemp("test_files")

View file

@ -2,12 +2,17 @@
Test the elicitation feature using stdio transport.
"""
from typing import Any
import pytest
from pydantic import BaseModel, Field
from mcp.client.session import ClientSession, ElicitationFnT
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
from mcp.shared.context import RequestContext
from mcp.shared.memory import create_connected_server_and_client_session
from mcp.types import ElicitResult, TextContent
from mcp.types import ElicitRequestParams, ElicitResult, TextContent
# Shared schema for basic tests
@ -19,11 +24,8 @@ def create_ask_user_tool(mcp: FastMCP):
"""Create a standard ask_user tool that handles all elicitation responses."""
@mcp.tool(description="A tool that uses elicitation")
async def ask_user(prompt: str, ctx: Context) -> str:
result = await ctx.elicit(
message=f"Tool wants to ask: {prompt}",
schema=AnswerSchema,
)
async def ask_user(prompt: str, ctx: Context[ServerSession, None]) -> str:
result = await ctx.elicit(message=f"Tool wants to ask: {prompt}", schema=AnswerSchema)
if result.action == "accept" and result.data:
return f"User answered: {result.data.answer}"
@ -37,9 +39,9 @@ def create_ask_user_tool(mcp: FastMCP):
async def call_tool_and_assert(
mcp: FastMCP,
elicitation_callback,
elicitation_callback: ElicitationFnT,
tool_name: str,
args: dict,
args: dict[str, Any],
expected_text: str | None = None,
text_contains: list[str] | None = None,
):
@ -69,7 +71,7 @@ async def test_stdio_elicitation():
create_ask_user_tool(mcp)
# Create a custom handler for elicitation requests
async def elicitation_callback(context, params):
async def elicitation_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
if params.message == "Tool wants to ask: What is your name?":
return ElicitResult(action="accept", content={"answer": "Test User"})
else:
@ -86,7 +88,7 @@ async def test_stdio_elicitation_decline():
mcp = FastMCP(name="StdioElicitationDeclineServer")
create_ask_user_tool(mcp)
async def elicitation_callback(context, params):
async def elicitation_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
return ElicitResult(action="decline")
await call_tool_and_assert(
@ -101,7 +103,7 @@ async def test_elicitation_schema_validation():
def create_validation_tool(name: str, schema_class: type[BaseModel]):
@mcp.tool(name=name, description=f"Tool testing {name}")
async def tool(ctx: Context) -> str:
async def tool(ctx: Context[ServerSession, None]) -> str:
try:
await ctx.elicit(message="This should fail validation", schema=schema_class)
return "Should not reach here"
@ -124,7 +126,7 @@ async def test_elicitation_schema_validation():
create_validation_tool("nested_model", InvalidNestedSchema)
# Dummy callback (won't be called due to validation failure)
async def elicitation_callback(context, params):
async def elicitation_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
return ElicitResult(action="accept", content={})
async with create_connected_server_and_client_session(
@ -153,7 +155,7 @@ async def test_elicitation_with_optional_fields():
subscribe: bool | None = Field(default=False, description="Subscribe to newsletter?")
@mcp.tool(description="Tool with optional fields")
async def optional_tool(ctx: Context) -> str:
async def optional_tool(ctx: Context[ServerSession, None]) -> str:
result = await ctx.elicit(message="Please provide your information", schema=OptionalSchema)
if result.action == "accept" and result.data:
@ -168,7 +170,7 @@ async def test_elicitation_with_optional_fields():
return f"User {result.action}"
# Test cases with different field combinations
test_cases = [
test_cases: list[tuple[dict[str, Any], str]] = [
(
# All fields provided
{"required_name": "John Doe", "optional_age": 30, "optional_email": "john@example.com", "subscribe": True},
@ -183,7 +185,7 @@ async def test_elicitation_with_optional_fields():
for content, expected in test_cases:
async def callback(context, params):
async def callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
return ElicitResult(action="accept", content=content)
await call_tool_and_assert(mcp, callback, "optional_tool", {}, expected)
@ -194,16 +196,19 @@ async def test_elicitation_with_optional_fields():
optional_list: list[str] | None = Field(default=None, description="Invalid optional list")
@mcp.tool(description="Tool with invalid optional field")
async def invalid_optional_tool(ctx: Context) -> str:
async def invalid_optional_tool(ctx: Context[ServerSession, None]) -> str:
try:
await ctx.elicit(message="This should fail", schema=InvalidOptionalSchema)
return "Should not reach here"
except TypeError as e:
return f"Validation failed: {str(e)}"
async def elicitation_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
return ElicitResult(action="accept", content={})
await call_tool_and_assert(
mcp,
lambda c, p: ElicitResult(action="accept", content={}),
elicitation_callback,
"invalid_optional_tool",
{},
text_contains=["Validation failed:", "optional_list"],

View file

@ -1,3 +1,9 @@
# NOTE: Those were added because we actually want to test wrong type annotations.
# pyright: reportUnknownParameterType=false
# pyright: reportMissingParameterType=false
# pyright: reportUnknownArgumentType=false
# pyright: reportUnknownLambdaType=false
from collections.abc import Callable
from dataclasses import dataclass
from typing import Annotated, Any, TypedDict
@ -58,7 +64,7 @@ def complex_arguments_fn(
an_int_with_equals_field: int = Field(1, ge=0),
int_annotated_with_default: Annotated[int, Field(description="hey")] = 5,
) -> str:
_ = (
_: Any = (
an_int,
must_be_none,
must_be_none_dumb_annotation,
@ -240,7 +246,7 @@ def test_structured_output_dict_str_types():
@pytest.mark.anyio
async def test_lambda_function():
"""Test lambda function schema and validation"""
fn = lambda x, y=5: x # noqa: E731
fn: Callable[[str, int], str] = lambda x, y=5: x # noqa: E731
meta = func_metadata(lambda x, y=5: x)
# Test schema
@ -899,7 +905,7 @@ def test_structured_output_unserializable_type_error():
class ConfigWithCallable:
name: str
# Callable defaults are not JSON serializable and will trigger Pydantic warnings
callback: Any = lambda x: x * 2
callback: Callable[[Any], Any] = lambda x: x * 2
def func_returning_config_with_callable() -> ConfigWithCallable:
return ConfigWithCallable()
@ -955,7 +961,7 @@ def test_structured_output_aliases():
# Check that the actual output uses aliases too
result = ModelWithAliases(**{"first": "hello", "second": "world"})
unstructured_content, structured_content = meta.convert_result(result)
_, structured_content = meta.convert_result(result)
# The structured content should use aliases to match the schema
assert "first" in structured_content
@ -967,7 +973,7 @@ def test_structured_output_aliases():
# Also test the case where we have a model with defaults to ensure aliases work in all cases
result_with_defaults = ModelWithAliases() # Uses default None values
unstructured_content_defaults, structured_content_defaults = meta.convert_result(result_with_defaults)
_, structured_content_defaults = meta.convert_result(result_with_defaults)
# Even with defaults, should use aliases in output
assert "first" in structured_content_defaults

View file

@ -4,6 +4,11 @@ Integration tests for FastMCP server functionality.
These tests validate the proper functioning of FastMCP features using focused,
single-feature servers across different transports (SSE and StreamableHTTP).
"""
# TODO(Marcelo): The `examples` package is not being imported as package. We need to solve this.
# pyright: reportUnknownMemberType=false
# pyright: reportMissingImports=false
# pyright: reportUnknownVariableType=false
# pyright: reportUnknownArgumentType=false
import json
import multiprocessing
@ -13,6 +18,7 @@ from collections.abc import Generator
import pytest
import uvicorn
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from pydantic import AnyUrl
from examples.snippets.servers import (
@ -29,17 +35,27 @@ from examples.snippets.servers import (
)
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamablehttp_client
from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client
from mcp.shared.context import RequestContext
from mcp.shared.message import SessionMessage
from mcp.shared.session import RequestResponder
from mcp.types import (
ClientResult,
CreateMessageRequestParams,
CreateMessageResult,
ElicitRequestParams,
ElicitResult,
GetPromptResult,
InitializeResult,
LoggingMessageNotification,
LoggingMessageNotificationParams,
NotificationParams,
ProgressNotification,
ProgressNotificationParams,
ReadResourceResult,
ResourceListChangedNotification,
ServerNotification,
ServerRequest,
TextContent,
TextResourceContents,
ToolListChangedNotification,
@ -50,12 +66,14 @@ class NotificationCollector:
"""Collects notifications from the server for testing."""
def __init__(self):
self.progress_notifications: list = []
self.log_messages: list = []
self.resource_notifications: list = []
self.tool_notifications: list = []
self.progress_notifications: list[ProgressNotificationParams] = []
self.log_messages: list[LoggingMessageNotificationParams] = []
self.resource_notifications: list[NotificationParams | None] = []
self.tool_notifications: list[NotificationParams | None] = []
async def handle_generic_notification(self, message) -> None:
async def handle_generic_notification(
self, message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception
) -> None:
"""Handle any server notification and route to appropriate handler."""
if isinstance(message, ServerNotification):
if isinstance(message.root, ProgressNotification):
@ -123,7 +141,7 @@ def run_server_with_transport(module_name: str, port: int, transport: str) -> No
@pytest.fixture
def server_transport(request, server_port: int) -> Generator[str, None, None]:
def server_transport(request: pytest.FixtureRequest, server_port: int) -> Generator[str, None, None]:
"""Start server in a separate process with specified MCP instance and transport.
Args:
@ -177,7 +195,14 @@ def create_client_for_transport(transport: str, server_url: str):
raise ValueError(f"Invalid transport: {transport}")
def unpack_streams(client_streams):
def unpack_streams(
client_streams: tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]]
| tuple[
MemoryObjectReceiveStream[SessionMessage | Exception],
MemoryObjectSendStream[SessionMessage],
GetSessionIdCallback,
],
):
"""Unpack client streams handling different return values from SSE vs StreamableHTTP.
SSE client returns (read_stream, write_stream)
@ -197,7 +222,9 @@ def unpack_streams(client_streams):
# Callback functions for testing
async def sampling_callback(context, params) -> CreateMessageResult:
async def sampling_callback(
context: RequestContext[ClientSession, None], params: CreateMessageRequestParams
) -> CreateMessageResult:
"""Sampling callback for tests."""
return CreateMessageResult(
role="assistant",
@ -209,7 +236,7 @@ async def sampling_callback(context, params) -> CreateMessageResult:
)
async def elicitation_callback(context, params):
async def elicitation_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams):
"""Elicitation callback for tests."""
# For restaurant booking test
if "No tables available" in params.message:
@ -367,7 +394,7 @@ async def test_tool_progress(server_transport: str, server_url: str) -> None:
transport = server_transport
collector = NotificationCollector()
async def message_handler(message):
async def message_handler(message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception):
await collector.handle_generic_notification(message)
if isinstance(message, Exception):
raise message
@ -508,7 +535,7 @@ async def test_notifications(server_transport: str, server_url: str) -> None:
transport = server_transport
collector = NotificationCollector()
async def message_handler(message):
async def message_handler(message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception):
await collector.handle_generic_notification(message)
if isinstance(message, Exception):
raise message

View file

@ -11,6 +11,7 @@ from mcp.server.fastmcp import Context, FastMCP
from mcp.server.fastmcp.prompts.base import Message, UserMessage
from mcp.server.fastmcp.resources import FileResource, FunctionResource
from mcp.server.fastmcp.utilities.types import Image
from mcp.server.session import ServerSession
from mcp.shared.exceptions import McpError
from mcp.shared.memory import (
create_connected_server_and_client_session as client_session,
@ -338,8 +339,10 @@ class TestServerTools:
image_path = tmp_path / "test.png"
image_path.write_bytes(b"test image data")
def mixed_list_fn() -> list:
return [
# TODO(Marcelo): It seems if we add the proper type hint, it generates an invalid JSON schema.
# We need to fix this.
def mixed_list_fn() -> list: # type: ignore
return [ # type: ignore
"text message",
Image(image_path),
{"key": "value"},
@ -347,7 +350,7 @@ class TestServerTools:
]
mcp = FastMCP()
mcp.add_tool(mixed_list_fn)
mcp.add_tool(mixed_list_fn) # type: ignore
async with client_session(mcp._mcp_server) as client:
result = await client.call_tool("mixed_list_fn", {})
assert len(result.content) == 4
@ -655,7 +658,7 @@ class TestServerResourceTemplates:
mcp = FastMCP()
@mcp.resource("resource://{param}")
def get_data(param) -> str:
def get_data(param) -> str: # type: ignore
return "Data"
@pytest.mark.anyio
@ -748,7 +751,7 @@ class TestContextInjection:
"""Test that context parameters are properly detected."""
mcp = FastMCP()
def tool_with_context(x: int, ctx: Context) -> str:
def tool_with_context(x: int, ctx: Context[ServerSession, None]) -> str:
return f"Request {ctx.request_id}: {x}"
tool = mcp._tool_manager.add_tool(tool_with_context)
@ -759,7 +762,7 @@ class TestContextInjection:
"""Test that context is properly injected into tool calls."""
mcp = FastMCP()
def tool_with_context(x: int, ctx: Context) -> str:
def tool_with_context(x: int, ctx: Context[ServerSession, None]) -> str:
assert ctx.request_id is not None
return f"Request {ctx.request_id}: {x}"
@ -777,7 +780,7 @@ class TestContextInjection:
"""Test that context works in async functions."""
mcp = FastMCP()
async def async_tool(x: int, ctx: Context) -> str:
async def async_tool(x: int, ctx: Context[ServerSession, None]) -> str:
assert ctx.request_id is not None
return f"Async request {ctx.request_id}: {x}"
@ -792,12 +795,10 @@ class TestContextInjection:
@pytest.mark.anyio
async def test_context_logging(self):
import mcp.server.session
"""Test that context logging methods work."""
mcp = FastMCP()
async def logging_tool(msg: str, ctx: Context) -> str:
async def logging_tool(msg: str, ctx: Context[ServerSession, None]) -> str:
await ctx.debug("Debug message")
await ctx.info("Info message")
await ctx.warning("Warning message")
@ -866,7 +867,7 @@ class TestContextInjection:
return "resource data"
@mcp.tool()
async def tool_with_resource(ctx: Context) -> str:
async def tool_with_resource(ctx: Context[ServerSession, None]) -> str:
r_iter = await ctx.read_resource("test://data")
r_list = list(r_iter)
assert len(r_list) == 1

View file

@ -34,7 +34,7 @@ class TestAddTools:
assert tool.parameters["properties"]["a"]["type"] == "integer"
assert tool.parameters["properties"]["b"]["type"] == "integer"
def test_init_with_tools(self, caplog):
def test_init_with_tools(self, caplog: pytest.LogCaptureFixture):
def sum(a: int, b: int) -> int:
return a + b
@ -89,7 +89,7 @@ class TestAddTools:
name: str
age: int
def create_user(user: UserInput, flag: bool) -> dict:
def create_user(user: UserInput, flag: bool) -> dict[str, Any]:
"""Create a new user."""
return {"id": 1, **user.model_dump()}
@ -145,15 +145,15 @@ class TestAddTools:
def test_add_lambda(self):
manager = ToolManager()
tool = manager.add_tool(lambda x: x, name="my_tool")
tool = manager.add_tool(lambda x: x, name="my_tool") # type: ignore[reportUnknownLambdaType]
assert tool.name == "my_tool"
def test_add_lambda_with_no_name(self):
manager = ToolManager()
with pytest.raises(ValueError, match="You must provide a name for lambda functions"):
manager.add_tool(lambda x: x)
manager.add_tool(lambda x: x) # type: ignore[reportUnknownLambdaType]
def test_warn_on_duplicate_tools(self, caplog):
def test_warn_on_duplicate_tools(self, caplog: pytest.LogCaptureFixture):
"""Test warning on duplicate tools."""
def f(x: int) -> int:
@ -165,7 +165,7 @@ class TestAddTools:
manager.add_tool(f)
assert "Tool already exists: f" in caplog.text
def test_disable_warn_on_duplicate_tools(self, caplog):
def test_disable_warn_on_duplicate_tools(self, caplog: pytest.LogCaptureFixture):
"""Test disabling warning on duplicate tools."""
def f(x: int) -> int:
@ -297,7 +297,7 @@ class TestCallTools:
shrimp: list[Shrimp]
x: None
def name_shrimp(tank: MyShrimpTank, ctx: Context) -> list[str]:
def name_shrimp(tank: MyShrimpTank, ctx: Context[ServerSessionT, None]) -> list[str]:
return [x.name for x in tank.shrimp]
manager = ToolManager()
@ -317,7 +317,7 @@ class TestCallTools:
class TestToolSchema:
@pytest.mark.anyio
async def test_context_arg_excluded_from_schema(self):
def something(a: int, ctx: Context) -> int:
def something(a: int, ctx: Context[ServerSessionT, None]) -> int:
return a
manager = ToolManager()
@ -334,7 +334,7 @@ class TestContextHandling:
"""Test that context parameters are properly detected in
Tool.from_function()."""
def tool_with_context(x: int, ctx: Context) -> str:
def tool_with_context(x: int, ctx: Context[ServerSessionT, None]) -> str:
return str(x)
manager = ToolManager()
@ -357,7 +357,7 @@ class TestContextHandling:
async def test_context_injection(self):
"""Test that context is properly injected during tool execution."""
def tool_with_context(x: int, ctx: Context) -> str:
def tool_with_context(x: int, ctx: Context[ServerSessionT, None]) -> str:
assert isinstance(ctx, Context)
return str(x)
@ -373,7 +373,7 @@ class TestContextHandling:
async def test_context_injection_async(self):
"""Test that context is properly injected in async tools."""
async def async_tool(x: int, ctx: Context) -> str:
async def async_tool(x: int, ctx: Context[ServerSessionT, None]) -> str:
assert isinstance(ctx, Context)
return str(x)
@ -389,7 +389,7 @@ class TestContextHandling:
async def test_context_optional(self):
"""Test that context is optional when calling tools."""
def tool_with_context(x: int, ctx: Context | None = None) -> str:
def tool_with_context(x: int, ctx: Context[ServerSessionT, None] | None = None) -> str:
return str(x)
manager = ToolManager()
@ -402,7 +402,7 @@ class TestContextHandling:
async def test_context_error_handling(self):
"""Test error handling when context injection fails."""
def tool_with_context(x: int, ctx: Context) -> str:
def tool_with_context(x: int, ctx: Context[ServerSessionT, None]) -> str:
raise ValueError("Test error")
manager = ToolManager()
@ -552,7 +552,7 @@ class TestStructuredOutput:
async def test_tool_without_structured_output(self):
"""Test that tools work normally when structured_output=False."""
def get_dict() -> dict:
def get_dict() -> dict[str, Any]:
"""Get a dict."""
return {"key": "value"}

View file

@ -1,5 +1,7 @@
"""Test that cancelled requests don't cause double responses."""
from typing import Any
import anyio
import pytest
@ -41,7 +43,7 @@ async def test_server_remains_functional_after_cancel():
]
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict | None) -> list:
async def handle_call_tool(name: str, arguments: dict[str, Any] | None) -> list[types.TextContent]:
nonlocal call_count, first_request_id
if name == "test_tool":
call_count += 1

View file

@ -2,6 +2,8 @@
Tests for completion handler with context functionality.
"""
from typing import Any
import pytest
from mcp.server.lowlevel import Server
@ -21,7 +23,7 @@ async def test_completion_handler_receives_context():
server = Server("test-server")
# Track what the handler receives
received_args = {}
received_args: dict[str, Any] = {}
@server.completion()
async def handle_completion(

View file

@ -2,6 +2,7 @@
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any
import anyio
import pytest
@ -10,6 +11,7 @@ from pydantic import TypeAdapter
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.lowlevel.server import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from mcp.server.session import ServerSession
from mcp.shared.message import SessionMessage
from mcp.types import (
ClientCapabilities,
@ -18,6 +20,8 @@ from mcp.types import (
JSONRPCMessage,
JSONRPCNotification,
JSONRPCRequest,
JSONRPCResponse,
TextContent,
)
@ -35,29 +39,23 @@ async def test_lowlevel_server_lifespan():
finally:
context["shutdown"] = True
server = Server("test", lifespan=test_lifespan)
server = Server[dict[str, bool]]("test", lifespan=test_lifespan)
# Create memory streams for testing
send_stream1, receive_stream1 = anyio.create_memory_object_stream(100)
send_stream2, receive_stream2 = anyio.create_memory_object_stream(100)
send_stream1, receive_stream1 = anyio.create_memory_object_stream[SessionMessage](100)
send_stream2, receive_stream2 = anyio.create_memory_object_stream[SessionMessage](100)
# Create a tool that accesses lifespan context
@server.call_tool()
async def check_lifespan(name: str, arguments: dict) -> list:
async def check_lifespan(name: str, arguments: dict[str, Any]) -> list[TextContent]:
ctx = server.request_context
assert isinstance(ctx.lifespan_context, dict)
assert ctx.lifespan_context["started"]
assert not ctx.lifespan_context["shutdown"]
return [{"type": "text", "text": "true"}]
return [TextContent(type="text", text="true")]
# Run server in background task
async with (
anyio.create_task_group() as tg,
send_stream1,
receive_stream1,
send_stream2,
receive_stream2,
):
async with anyio.create_task_group() as tg, send_stream1, receive_stream1, send_stream2, receive_stream2:
async def run_server():
await server.run(
@ -126,6 +124,8 @@ async def test_lowlevel_server_lifespan():
# Get response and verify
response = await receive_stream2.receive()
response = response.message
assert isinstance(response, JSONRPCMessage)
assert isinstance(response.root, JSONRPCResponse)
assert response.root.result["content"][0]["text"] == "true"
# Cancel server task
@ -137,7 +137,7 @@ async def test_fastmcp_server_lifespan():
"""Test that lifespan works in FastMCP server."""
@asynccontextmanager
async def test_lifespan(server: FastMCP) -> AsyncIterator[dict]:
async def test_lifespan(server: FastMCP) -> AsyncIterator[dict[str, bool]]:
"""Test lifespan context that tracks startup/shutdown."""
context = {"started": False, "shutdown": False}
try:
@ -149,12 +149,12 @@ async def test_fastmcp_server_lifespan():
server = FastMCP("test", lifespan=test_lifespan)
# Create memory streams for testing
send_stream1, receive_stream1 = anyio.create_memory_object_stream(100)
send_stream2, receive_stream2 = anyio.create_memory_object_stream(100)
send_stream1, receive_stream1 = anyio.create_memory_object_stream[SessionMessage](100)
send_stream2, receive_stream2 = anyio.create_memory_object_stream[SessionMessage](100)
# Add a tool that checks lifespan context
@server.tool()
def check_lifespan(ctx: Context) -> bool:
def check_lifespan(ctx: Context[ServerSession, None]) -> bool:
"""Tool that checks lifespan context."""
assert isinstance(ctx.request_context.lifespan_context, dict)
assert ctx.request_context.lifespan_context["started"]
@ -230,6 +230,8 @@ async def test_fastmcp_server_lifespan():
# Get response and verify
response = await receive_stream2.receive()
response = response.message
assert isinstance(response, JSONRPCMessage)
assert isinstance(response.root, JSONRPCResponse)
assert response.root.result["content"][0]["text"] == "true"
# Cancel server task

View file

@ -263,7 +263,7 @@ async def test_enum_constraint_validation():
@pytest.mark.anyio
async def test_tool_not_in_list_logs_warning(caplog):
async def test_tool_not_in_list_logs_warning(caplog: pytest.LogCaptureFixture):
"""Test that calling a tool not in list_tools logs a warning and skips validation."""
tools = [
Tool(

View file

@ -1,3 +1,5 @@
from typing import Any
import anyio
import pytest
@ -16,8 +18,10 @@ from mcp.types import (
CompletionContext,
CompletionsCapability,
InitializedNotification,
Prompt,
PromptReference,
PromptsCapability,
Resource,
ResourcesCapability,
ResourceTemplateReference,
ServerCapabilities,
@ -80,7 +84,7 @@ async def test_server_session_initialize():
async def test_server_capabilities():
server = Server("test")
notification_options = NotificationOptions()
experimental_capabilities = {}
experimental_capabilities: dict[str, Any] = {}
# Initially no capabilities
caps = server.get_capabilities(notification_options, experimental_capabilities)
@ -90,7 +94,7 @@ async def test_server_capabilities():
# Add a prompts handler
@server.list_prompts()
async def list_prompts():
async def list_prompts() -> list[Prompt]:
return []
caps = server.get_capabilities(notification_options, experimental_capabilities)
@ -100,7 +104,7 @@ async def test_server_capabilities():
# Add a resources handler
@server.list_resources()
async def list_resources():
async def list_resources() -> list[Resource]:
return []
caps = server.get_capabilities(notification_options, experimental_capabilities)

View file

@ -26,7 +26,7 @@ async def test_stdio_server():
read_stream,
write_stream,
):
received_messages = []
received_messages: list[JSONRPCMessage] = []
async with read_stream:
async for message in read_stream:
if isinstance(message, Exception):

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