Fix type errors for ty 0.0.1-alpha.31 upgrade (#2561)

* Fix type errors for ty 0.0.1-alpha.31 upgrade

Add type ignores and fixes for ty's stricter checking:
- Path(None) guards in cli.py
- isinstance checks for ElicitRequestFormParams (URL elicitation support)
- TODO(ty) comments for match/isinstance narrowing bugs
- Method override type ignores for generic covariance
- Starlette Middleware typing workarounds
- Dynamic type construction ignores in json_schema_type.py

* Fix remaining type errors for ty 0.0.1-alpha.31

- Add asserts for optional attribute access in tests
- Add type ignores for dynamic httpx transport internals
- Add TODO(ty) comments for `in` operator on str|bytes
- Add TODO(ty) comments for Starlette Middleware typing
- Use cast for prompt.fn async validation in server.py

* Upgrade ty to 0.0.1-alpha.31

Fixes additional test file type errors discovered after upgrade.
This commit is contained in:
Jeremiah Lowin 2025-12-05 21:29:14 -05:00 committed by GitHub
commit 07750efaab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1115 additions and 893 deletions

View file

@ -72,7 +72,7 @@ dev = [
"pytest-timeout>=2.4.0",
"pytest-xdist>=3.6.1",
"ruff>=0.12.8",
"ty==0.0.1a25",
"ty==0.0.1a31",
"prek>=0.2.12",
]

View file

@ -103,7 +103,7 @@ def version(
"MCP version": importlib.metadata.version("mcp"),
"Python version": platform.python_version(),
"Platform": platform.platform(),
"FastMCP root path": Path(fastmcp.__file__).resolve().parents[1],
"FastMCP root path": Path(fastmcp.__file__ or ".").resolve().parents[1],
}
g = Table.grid(padding=(0, 1))
@ -819,11 +819,13 @@ async def prepare(
)
sys.exit(1)
assert config_path is not None
config_file = Path(config_path)
if not config_file.exists():
logger.error(f"Configuration file not found: {config_path}")
sys.exit(1)
assert output_dir is not None
output_path = Path(output_dir)
try:

View file

@ -7,7 +7,7 @@ import mcp.types
from mcp import ClientSession
from mcp.client.session import ElicitationFnT
from mcp.shared.context import LifespanContextT, RequestContext
from mcp.types import ElicitRequestParams
from mcp.types import ElicitRequestFormParams, ElicitRequestParams
from mcp.types import ElicitResult as MCPElicitResult
from pydantic_core import to_jsonable_python
from typing_extensions import TypeVar
@ -26,7 +26,8 @@ class ElicitResult(MCPElicitResult, Generic[T]):
ElicitationHandler: TypeAlias = Callable[
[
str, # message
type[T], # a class for creating a structured response
type[T]
| None, # a class for creating a structured response (None for URL elicitation)
ElicitRequestParams,
RequestContext[ClientSession, LifespanContextT],
],
@ -42,10 +43,15 @@ def create_elicitation_callback(
params: ElicitRequestParams,
) -> MCPElicitResult | mcp.types.ErrorData:
try:
if params.requestedSchema == {"type": "object", "properties": {}}:
response_type = None
# requestedSchema only exists on ElicitRequestFormParams, not ElicitRequestURLParams
if isinstance(params, ElicitRequestFormParams):
if params.requestedSchema == {"type": "object", "properties": {}}:
response_type = None
else:
response_type = json_schema_to_type(params.requestedSchema)
else:
response_type = json_schema_to_type(params.requestedSchema)
# URL-based elicitation doesn't have a schema
response_type = None
result = await elicitation_handler(
params.message, response_type, params, context

View file

@ -35,16 +35,18 @@ class MessageHandler:
# requests
case RequestResponder():
# handle all requests
await self.on_request(message)
# TODO(ty): remove when ty supports match statement narrowing
await self.on_request(message) # type: ignore[arg-type]
# handle specific requests
match message.request.root:
# TODO(ty): remove type ignores when ty supports match statement narrowing
match message.request.root: # type: ignore[union-attr]
case mcp.types.PingRequest():
await self.on_ping(message.request.root)
await self.on_ping(message.request.root) # type: ignore[union-attr]
case mcp.types.ListRootsRequest():
await self.on_list_roots(message.request.root)
await self.on_list_roots(message.request.root) # type: ignore[union-attr]
case mcp.types.CreateMessageRequest():
await self.on_create_message(message.request.root)
await self.on_create_message(message.request.root) # type: ignore[union-attr]
# notifications
case mcp.types.ServerNotification():

View file

@ -34,7 +34,8 @@ def create_roots_callback(
handler: RootsList | RootsHandler,
) -> ListRootsFnT:
if isinstance(handler, list):
return _create_roots_callback_from_roots(handler)
# TODO(ty): remove when ty supports isinstance union narrowing
return _create_roots_callback_from_roots(handler) # type: ignore[arg-type]
elif inspect.isfunction(handler):
return _create_roots_callback_from_fn(handler)
else:

View file

@ -201,7 +201,7 @@ class Task(abc.ABC, Generic[TaskResultT]):
result = callback(status)
if inspect.isawaitable(result):
# Fire and forget async callbacks
asyncio.create_task(result) # noqa: RUF006
asyncio.create_task(result) # type: ignore[arg-type] # noqa: RUF006
except Exception as e:
logger.warning(f"Task callback error: {e}", exc_info=True)

View file

@ -375,7 +375,8 @@ class StdioTransport(ClientTransport):
env=self.env,
cwd=self.cwd,
log_file=self.log_file,
session_kwargs=session_kwargs,
# TODO(ty): remove when ty supports Unpack[TypedDict] inference
session_kwargs=session_kwargs, # type: ignore[arg-type]
ready_event=self._ready_event,
stop_event=self._stop_event,
session_future=session_future,

View file

@ -164,7 +164,7 @@ class OpenAISamplingHandler(BaseLLMSamplingHandler):
) -> ChatModel:
for model_option in self._iter_models_from_preferences(model_preferences):
if model_option in get_args(ChatModel):
chosen_model: ChatModel = model_option # pyright: ignore[reportAssignmentType]
chosen_model: ChatModel = model_option # type: ignore[assignment]
return chosen_model
return self.default_model

View file

@ -206,7 +206,7 @@ class FunctionPrompt(Prompt):
fn = fn.__call__
# if the fn is a staticmethod, we need to work with the underlying function
if isinstance(fn, staticmethod):
fn = fn.__func__
fn = fn.__func__ # type: ignore[assignment]
# Validate that task=True requires async functions (after unwrapping)
if task and not inspect.iscoroutinefunction(fn):

View file

@ -189,12 +189,13 @@ class AuthProvider(TokenVerifierProtocol):
Returns:
List of Starlette Middleware instances to apply to the HTTP app
"""
# TODO(ty): remove type ignores when ty supports Starlette Middleware typing
return [
Middleware(
AuthenticationMiddleware,
AuthenticationMiddleware, # type: ignore[arg-type]
backend=BearerAuthBackend(self),
),
Middleware(AuthContextMiddleware),
Middleware(AuthContextMiddleware), # type: ignore[arg-type]
]
def _get_resource_url(self, path: str | None = None) -> AnyHttpUrl | None:

View file

@ -1006,7 +1006,8 @@ class OAuthProxy(OAuthProvider):
# Store transaction data for IdP callback processing
if client.client_id is None:
raise AuthorizeError(
error="invalid_client", error_description="Client ID is required"
error="invalid_client", # type: ignore[arg-type]
error_description="Client ID is required",
)
transaction = OAuthTransaction(
txn_id=txn_id,
@ -1088,7 +1089,8 @@ class OAuthProxy(OAuthProvider):
# Create authorization code object with PKCE challenge
if client.client_id is None:
raise AuthorizeError(
error="invalid_client", error_description="Client ID is required"
error="invalid_client", # type: ignore[arg-type]
error_description="Client ID is required",
)
return AuthorizationCode(
code=authorization_code,
@ -1512,7 +1514,7 @@ class OAuthProxy(OAuthProvider):
# Token Validation
# -------------------------------------------------------------------------
async def load_access_token(self, token: str) -> AccessToken | None:
async def load_access_token(self, token: str) -> AccessToken | None: # type: ignore[override]
"""Validate FastMCP JWT by swapping for upstream token.
This implements the token swap pattern:

View file

@ -284,7 +284,7 @@ class InMemoryOAuthProvider(OAuthProvider):
scope=" ".join(scopes),
)
async def load_access_token(self, token: str) -> AccessToken | None:
async def load_access_token(self, token: str) -> AccessToken | None: # type: ignore[override]
token_obj = self.access_tokens.get(token)
if token_obj:
if token_obj.expires_at is not None and token_obj.expires_at < time.time():
@ -295,7 +295,7 @@ class InMemoryOAuthProvider(OAuthProvider):
return token_obj
return None
async def verify_token(self, token: str) -> AccessToken | None:
async def verify_token(self, token: str) -> AccessToken | None: # type: ignore[override]
"""
Verify a bearer token and return access info if valid.

View file

@ -171,7 +171,7 @@ class OCIProvider(OIDCProxy):
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
"""
overrides = {
overrides: dict[str, object] = {
k: v
for k, v in {
"config_url": config_url,
@ -187,7 +187,7 @@ class OCIProvider(OIDCProxy):
}.items()
if v is not NotSet
}
settings = OCIProviderSettings(**overrides)
settings = OCIProviderSettings(**overrides) # type: ignore[arg-type]
if not settings.config_url:
raise ValueError(

View file

@ -388,7 +388,7 @@ class Context:
session_id = str(uuid4())
# Save the session id to the session attributes
session._fastmcp_id = session_id
session._fastmcp_id = session_id # type: ignore[attr-defined]
return session_id
@property
@ -540,7 +540,7 @@ class Context:
maxTokens=max_tokens,
modelPreferences=_parse_model_preferences(model_preferences),
),
self.request_context,
self.request_context, # type: ignore[arg-type]
)
if inspect.isawaitable(create_message_result):

View file

@ -37,13 +37,13 @@ class ElicitationJsonSchema(GenerateJsonSchema):
Optionally adds enumNames for better UI display when available.
"""
def generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue:
def generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue: # type: ignore[override]
"""Override to prevent ref generation for enums."""
# For enum schemas, bypass the ref mechanism entirely
if schema["type"] == "enum":
# Directly call our custom enum_schema without going through handler
# This prevents the ref/defs mechanism from being invoked
return self.enum_schema(schema)
return self.enum_schema(schema) # type: ignore[arg-type]
# For all other types, use the default implementation
return super().generate_inner(schema)

View file

@ -117,7 +117,8 @@ def create_base_app(
A Starlette application
"""
# Always add RequestContextMiddleware as the outermost middleware
middleware.insert(0, Middleware(RequestContextMiddleware))
# TODO(ty): remove type ignore when ty supports Starlette Middleware typing
middleware.insert(0, Middleware(RequestContextMiddleware)) # type: ignore[arg-type]
return StarletteWithLifespan(
routes=routes,

View file

@ -14,6 +14,7 @@ from mcp.shared.exceptions import McpError
from mcp.types import (
METHOD_NOT_FOUND,
BlobResourceContents,
ElicitRequestFormParams,
GetPromptResult,
TextResourceContents,
)
@ -363,7 +364,7 @@ class ProxyTemplate(ResourceTemplate, MirroredComponent):
self._client = client
@classmethod
def from_mcp_template(
def from_mcp_template( # type: ignore[override]
cls, client: Client, mcp_template: mcp.types.ResourceTemplate
) -> ProxyTemplate:
"""Factory method to create a ProxyTemplate from a raw MCP template schema."""
@ -454,7 +455,7 @@ class ProxyPrompt(Prompt, MirroredComponent):
_mirrored=True,
)
async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]: # type: ignore[override]
"""Render the prompt by making a call through the client."""
async with self._client:
result = await self._client.get_prompt(self.name, arguments)
@ -567,7 +568,8 @@ class ProxyClient(Client[ClientTransportT]):
return mcp.types.CreateMessageResult(
role="assistant",
model="fastmcp-client",
content=content,
# TODO(ty): remove when ty supports isinstance exclusion narrowing
content=content, # type: ignore[arg-type]
)
@classmethod
@ -582,9 +584,15 @@ class ProxyClient(Client[ClientTransportT]):
A handler that forwards the elicitation request from the remote server to the proxy's connected clients and relays the response back to the remote server.
"""
ctx = get_context()
# requestedSchema only exists on ElicitRequestFormParams, not ElicitRequestURLParams
requested_schema = (
params.requestedSchema
if isinstance(params, ElicitRequestFormParams)
else {"type": "object", "properties": {}}
)
result = await ctx.session.elicit(
message=message,
requestedSchema=params.requestedSchema,
requestedSchema=requested_schema,
related_request_id=ctx.request_id,
)
return ElicitResult(action=result.action, content=result.content)
@ -628,7 +636,7 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
super().__init__(*args, **kwargs)
self._caches: dict[ServerSession, Client[ClientTransportT]] = {}
async def __aexit__(self, exc_type, exc_value, traceback) -> None:
async def __aexit__(self, exc_type, exc_value, traceback) -> None: # type: ignore[override]
"""
The stateful proxy client will be forced disconnected when the session is exited.
So we do nothing here.

View file

@ -407,7 +407,8 @@ class FastMCP(Generic[LifespanResultT]):
for prompt in self._prompt_manager._prompts.values():
if isinstance(prompt, FunctionPrompt) and prompt.task:
docket.register(prompt.fn)
# task=True requires async fn (validated at creation time)
docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
for resource in self._resource_manager._resources.values():
if isinstance(resource, FunctionResource) and resource.task:

View file

@ -96,7 +96,7 @@ class FastMCPComponent(FastMCPBaseModel):
return meta or None
def model_copy(
def model_copy( # type: ignore[override]
self,
*,
update: dict[str, Any] | None = None,
@ -137,7 +137,7 @@ class FastMCPComponent(FastMCPBaseModel):
"""Disable the component."""
self.enabled = False
def copy(self) -> Self:
def copy(self) -> Self: # type: ignore[override]
"""Create a copy of the component."""
return self.model_copy()
@ -173,7 +173,7 @@ class MirroredComponent(FastMCPComponent):
)
super().disable()
def copy(self) -> Self:
def copy(self) -> Self: # type: ignore[override]
"""Create a copy of the component that can be modified."""
# Create a copy and mark it as not mirrored
copied = self.model_copy()

View file

@ -248,7 +248,7 @@ def _create_numeric_type(
if v is not None
}
return Annotated[base, Field(**constraints)] if constraints else base
return Annotated[base, Field(**constraints)] if constraints else base # type: ignore[return-value]
def _create_enum(name: str, values: list[Any]) -> type:
@ -265,8 +265,8 @@ def _create_array_type(
if isinstance(items, list):
# Handle positional item schemas
item_types = [_schema_to_type(s, schemas) for s in items]
combined = Union[tuple(item_types)] # type: ignore # noqa: UP007
base = list[combined]
combined = Union[tuple(item_types)] # type: ignore[arg-type] # noqa: UP007
base = list[combined] # type: ignore[valid-type]
else:
# Handle single item schema
item_type = _schema_to_type(items, schemas)
@ -282,7 +282,7 @@ def _create_array_type(
if v is not None
}
return Annotated[base, Field(**constraints)] if constraints else base
return Annotated[base, Field(**constraints)] if constraints else base # type: ignore[return-value]
def _return_Any() -> Any:

View file

@ -192,7 +192,7 @@ class MCPServerConfig(BaseModel):
"""
if isinstance(v, dict):
return FileSystemSource(**v)
return v
return v # type: ignore[return-value]
@field_validator("environment", mode="before")
@classmethod

View file

@ -159,6 +159,7 @@ async def test_call_tool_with_meta():
from fastmcp.server.dependencies import get_context
context = get_context()
assert context.request_context is not None
meta = context.request_context.meta
# Return the meta data as a dict

View file

@ -3,7 +3,7 @@ from enum import Enum
from typing import Literal
import pytest
from mcp.types import ElicitRequestParams
from mcp.types import ElicitRequestFormParams, ElicitRequestParams
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
@ -153,6 +153,7 @@ class TestScalarResponseTypes:
async def elicitation_handler(
message, response_type, params: ElicitRequestParams, ctx
):
assert isinstance(params, ElicitRequestFormParams)
assert params.requestedSchema == {"type": "object", "properties": {}}
assert response_type is None
return ElicitResult(action="accept")

View file

@ -15,8 +15,9 @@ async def test_oauth_uses_same_client_as_transport_streamable_http():
)
async with transport.auth.httpx_client_factory() as httpx_client: # type: ignore[attr-defined]
assert httpx_client._transport is not None
assert (
httpx_client._transport._pool._ssl_context.verify_mode
httpx_client._transport._pool._ssl_context.verify_mode # type: ignore[attr-defined]
== VerifyMode.CERT_NONE
)
@ -31,7 +32,8 @@ async def test_oauth_uses_same_client_as_transport_sse():
)
async with transport.auth.httpx_client_factory() as httpx_client: # type: ignore[attr-defined]
assert httpx_client._transport is not None
assert (
httpx_client._transport._pool._ssl_context.verify_mode
httpx_client._transport._pool._ssl_context.verify_mode # type: ignore[attr-defined]
== VerifyMode.CERT_NONE
)

View file

@ -779,8 +779,9 @@ class TestQueryParameterTypeCoercion:
)
content = await resource.read()
assert '"page":5' in content
assert '"type":"int"' in content
# TODO(ty): remove when ty supports `in` on str | bytes
assert '"page":5' in content # type: ignore[operator]
assert '"type":"int"' in content # type: ignore[operator]
async def test_bool_coercion(self):
"""Test boolean type coercion for query parameters."""
@ -800,7 +801,8 @@ class TestQueryParameterTypeCoercion:
{"name": "feature", "enabled": "true"},
)
content = await resource.read()
assert '"enabled":true' in content
# TODO(ty): remove when ty supports `in` on str | bytes
assert '"enabled":true' in content # type: ignore[operator]
# Test false value
resource = await template.create_resource(
@ -808,7 +810,8 @@ class TestQueryParameterTypeCoercion:
{"name": "feature", "enabled": "false"},
)
content = await resource.read()
assert '"enabled":false' in content
# TODO(ty): remove when ty supports `in` on str | bytes
assert '"enabled":false' in content # type: ignore[operator]
async def test_float_coercion(self):
"""Test float type coercion for query parameters."""
@ -832,8 +835,9 @@ class TestQueryParameterTypeCoercion:
)
content = await resource.read()
assert '"threshold":0.95' in content
assert '"type":"float"' in content
# TODO(ty): remove when ty supports `in` on str | bytes
assert '"threshold":0.95' in content # type: ignore[operator]
assert '"type":"float"' in content # type: ignore[operator]
class TestQueryParameterValidation:
@ -892,8 +896,9 @@ class TestQueryParameterWithDefaults:
)
content = await resource.read()
assert '"format":"json"' in content
assert '"verbose":false' in content
# TODO(ty): remove when ty supports `in` on str | bytes
assert '"format":"json"' in content # type: ignore[operator]
assert '"verbose":false' in content # type: ignore[operator]
async def test_partial_query_params(self):
"""Test providing only some query parameters."""
@ -916,9 +921,10 @@ class TestQueryParameterWithDefaults:
)
content = await resource.read()
assert '"format":"json"' in content # default
assert '"limit":20' in content # provided
assert '"offset":0' in content # default
# TODO(ty): remove when ty supports `in` on str | bytes
assert '"format":"json"' in content # type: ignore[operator] # default
assert '"limit":20' in content # type: ignore[operator] # provided
assert '"offset":0' in content # type: ignore[operator] # default
class TestQueryParameterWithWildcards:
@ -951,6 +957,7 @@ class TestQueryParameterWithWildcards:
)
content = await resource.read()
assert '"path":"src/test/data.txt"' in content
assert '"encoding":"utf-8"' in content # default
assert '"lines":50' in content # provided
# TODO(ty): remove when ty supports `in` on str | bytes
assert '"path":"src/test/data.txt"' in content # type: ignore[operator]
assert '"encoding":"utf-8"' in content # type: ignore[operator] # default
assert '"lines":50' in content # type: ignore[operator] # provided

View file

@ -851,6 +851,7 @@ class TestOIDCScopeHandling:
# required_scopes (used for validation) excludes OIDC scopes
assert provider.required_scopes == ["read"]
# But valid_scopes (advertised to clients) includes all scopes
assert provider.client_registration_options is not None
assert provider.client_registration_options.valid_scopes == [
"read",
"openid",

View file

@ -550,7 +550,7 @@ class TestBearerTokenJWKS:
mock_jwks_data: JWKSData,
httpx_mock: HTTPXMock,
):
mock_jwks_data["keys"] = [
mock_jwks_data["keys"] = [ # type: ignore[typeddict-item]
{
"kid": "test-key-1",
"alg": "RS256",

View file

@ -14,7 +14,7 @@ class MockTokenVerifier(TokenVerifier):
def __init__(self):
self.required_scopes = []
async def verify_token(self, token: str) -> dict | None:
async def verify_token(self, token: str) -> dict | None: # type: ignore[override]
return {"sub": "test-user"}

View file

@ -55,9 +55,12 @@ async def test_sse_app_with_custom_middleware():
server = FastMCP(name="TestServer")
# Create custom middleware
# TODO(ty): remove when Starlette Middleware typing is supported
custom_middleware = [
Middleware(
HeaderMiddleware, header_name="X-Custom-Header", header_value="test-value"
HeaderMiddleware, # type: ignore[arg-type]
header_name="X-Custom-Header",
header_value="test-value",
)
]
@ -85,9 +88,12 @@ async def test_streamable_http_app_with_custom_middleware():
server = FastMCP(name="TestServer")
# Create custom middleware
# TODO(ty): remove when Starlette Middleware typing is supported
custom_middleware = [
Middleware(
HeaderMiddleware, header_name="X-Custom-Header", header_value="test-value"
HeaderMiddleware, # type: ignore[arg-type]
header_name="X-Custom-Header",
header_value="test-value",
)
]
@ -115,8 +121,13 @@ async def test_create_sse_app_with_custom_middleware():
server = FastMCP(name="TestServer")
# Create custom middleware
# TODO(ty): remove when Starlette Middleware typing is supported
custom_middleware = [
Middleware(RequestModifierMiddleware, key="modified_by", value="middleware")
Middleware(
RequestModifierMiddleware, # type: ignore[arg-type]
key="modified_by",
value="middleware",
)
]
# Add a test route
@ -150,8 +161,13 @@ async def test_create_streamable_http_app_with_custom_middleware():
server = FastMCP(name="TestServer")
# Create custom middleware
# TODO(ty): remove when Starlette Middleware typing is supported
custom_middleware = [
Middleware(RequestModifierMiddleware, key="modified_by", value="middleware")
Middleware(
RequestModifierMiddleware, # type: ignore[arg-type]
key="modified_by",
value="middleware",
)
]
# Add a test route
@ -184,12 +200,17 @@ async def test_multiple_middleware_ordering():
server = FastMCP(name="TestServer")
# Create multiple middleware
# TODO(ty): remove when Starlette Middleware typing is supported
custom_middleware = [
Middleware(
HeaderMiddleware, header_name="X-First-Header", header_value="first"
HeaderMiddleware, # type: ignore[arg-type]
header_name="X-First-Header",
header_value="first",
),
Middleware(
HeaderMiddleware, header_name="X-Second-Header", header_value="second"
HeaderMiddleware, # type: ignore[arg-type]
header_name="X-Second-Header",
header_value="second",
),
]

View file

@ -3,7 +3,13 @@ from typing import cast
import pytest
from anyio import create_task_group
from mcp.types import LoggingLevel, ModelHint, ModelPreferences, TextContent
from mcp.types import (
ElicitRequestFormParams,
LoggingLevel,
ModelHint,
ModelPreferences,
TextContent,
)
from pydantic import BaseModel, Field
from fastmcp import Client, Context, FastMCP
@ -181,6 +187,7 @@ class TestProxyClient:
elicitation_handler_called = True
assert message == "What is your name?"
assert "Person" in str(response_type)
assert isinstance(params, ElicitRequestFormParams)
assert params.requestedSchema == {
"title": "Person",
"type": "object",
@ -377,6 +384,7 @@ class TestProxyClient:
ctx: RequestContext,
):
# Verify the schema is correct - acknowledge should have default=False, not be nullable
assert isinstance(params, ElicitRequestFormParams)
schema = params.requestedSchema
assert schema["properties"]["acknowledge"]["type"] == "boolean"
assert schema["properties"]["acknowledge"]["default"] is False

View file

@ -143,7 +143,7 @@ async def test_proxy_with_async_client_factory():
proxy = FastMCPProxy(client_factory=async_factory)
assert isinstance(proxy, FastMCPProxy)
assert inspect.iscoroutinefunction(proxy.client_factory)
client = await proxy.client_factory()
client = await proxy.client_factory() # type: ignore[misc]
assert isinstance(client, Client)
assert isinstance(client.transport, StreamableHttpTransport)
assert client.transport.url == "http://example.com/mcp/" # type: ignore[attr-defined]

View file

@ -159,6 +159,7 @@ async def test_user_lifespan_still_works_with_docket():
def check_both(docket: Docket = CurrentDocket()) -> str:
assert isinstance(docket, Docket)
ctx = get_context()
assert ctx.request_context is not None
lifespan_data = ctx.request_context.lifespan_context
assert lifespan_data.get("custom_data") == "test_value"
return HUZZAH

View file

@ -62,6 +62,7 @@ class TestServerLifespan:
@mcp.tool
def get_db_info(ctx: Context) -> str:
# Access the server lifespan context
assert ctx.request_context is not None
lifespan_context = ctx.request_context.lifespan_context
return lifespan_context.get("db_connection", "no_db")

View file

@ -1149,7 +1149,7 @@ class TestEdgeCases:
Type = json_schema_to_type(schema)
validator = TypeAdapter(Type)
result = validator.validate_python({"name": "test"})
assert result.name == "test"
assert result.name == "test" # type: ignore[attr-defined]
def test_recursive_defaults(self):
schema = {
@ -1165,8 +1165,8 @@ class TestEdgeCases:
Type = json_schema_to_type(schema)
validator = TypeAdapter(Type)
result = validator.validate_python({})
assert result.node.value == "default"
assert result.node.next is None
assert result.node.value == "default" # type: ignore[attr-defined]
assert result.node.next is None # type: ignore[attr-defined]
def test_mixed_type_array(self):
schema = {
@ -1277,9 +1277,9 @@ class TestNameHandling:
result = validator.validate_python(
{"name": "parent", "child": {"name": "child", "child": None}}
)
assert result.name == "parent"
assert result.child.name == "child"
assert result.child.child is None
assert result.name == "parent" # type: ignore[attr-defined]
assert result.child.name == "child" # type: ignore[attr-defined]
assert result.child.child is None # type: ignore[attr-defined]
class TestAdditionalProperties:
@ -1459,17 +1459,17 @@ class TestAdditionalProperties:
result = validator.validate_python(data)
# Check top-level extra field (BaseModel)
assert result.top_level_extra == "preserved"
assert result.top_level_extra == "preserved" # type: ignore[attr-defined]
# Check nested user extra field (BaseModel)
assert result.user.name == "Alice"
assert result.user.extra_user_field == "value"
assert result.user.name == "Alice" # type: ignore[attr-defined]
assert result.user.extra_user_field == "value" # type: ignore[attr-defined]
# Check nested settings - should be dataclass
assert result.settings.theme == "dark"
assert result.settings.theme == "dark" # type: ignore[attr-defined]
# Note: When nested in BaseModel with extra='allow', Pydantic may preserve extra fields
# even on dataclass children. The important thing is that settings is still a dataclass.
assert not issubclass(type(result.settings), BaseModel)
assert not issubclass(type(result.settings), BaseModel) # type: ignore[attr-defined]
def test_additional_properties_false_vs_missing(self):
"""Test difference between additionalProperties: false and missing additionalProperties"""
@ -1511,9 +1511,9 @@ class TestAdditionalProperties:
# Test with extra fields and defaults
result = validator.validate_python({"extra": "field"})
assert result.name == "anonymous"
assert result.age == 0
assert result.extra == "field"
assert result.name == "anonymous" # type: ignore[attr-defined]
assert result.age == 0 # type: ignore[attr-defined]
assert result.extra == "field" # type: ignore[attr-defined]
def test_additional_properties_type_consistency(self):
"""Test that the same schema always returns the same type"""
@ -1570,7 +1570,7 @@ class TestFieldsWithDefaults:
validator = TypeAdapter(generated_type)
result = validator.validate_python({})
assert result.flag is False
assert result.flag is False # type: ignore[attr-defined]
def test_field_with_default_accepts_explicit_value(self):
"""Test that fields with defaults accept explicit values."""
@ -1583,4 +1583,4 @@ class TestFieldsWithDefaults:
validator = TypeAdapter(generated_type)
result = validator.validate_python({"flag": True})
assert result.flag is True
assert result.flag is True # type: ignore[attr-defined]

1712
uv.lock generated

File diff suppressed because it is too large Load diff