mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
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:
parent
1ed976b1d9
commit
07750efaab
35 changed files with 1115 additions and 893 deletions
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue