Get baseline 100% clean coverage (#1553)
This commit is contained in:
parent
da4fce2195
commit
89e9c43acf
29 changed files with 1032 additions and 1084 deletions
|
|
@ -50,7 +50,7 @@ class AuthorizationErrorResponse(BaseModel):
|
|||
|
||||
|
||||
def best_effort_extract_string(key: str, params: None | FormData | QueryParams) -> str | None:
|
||||
if params is None:
|
||||
if params is None: # pragma: no cover
|
||||
return None
|
||||
value = params.get(key)
|
||||
if isinstance(value, str):
|
||||
|
|
@ -116,7 +116,7 @@ class AuthorizationHandler:
|
|||
pass
|
||||
|
||||
# the error response MUST contain the state specified by the client, if any
|
||||
if state is None:
|
||||
if state is None: # pragma: no cover
|
||||
# make last-ditch effort to load state
|
||||
state = best_effort_extract_string("state", params)
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ class AuthorizationHandler:
|
|||
# Handle authorization errors as defined in RFC 6749 Section 4.1.2.1
|
||||
return await error_response(error=e.error, error_description=e.error_description)
|
||||
|
||||
except Exception as validation_error:
|
||||
except Exception as validation_error: # pragma: no cover
|
||||
# Catch-all for unexpected errors
|
||||
logger.exception("Unexpected error in authorization_handler", exc_info=validation_error)
|
||||
return await error_response(error="server_error", error_description="An unexpected error occurred")
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class RegistrationHandler:
|
|||
|
||||
client_id = str(uuid4())
|
||||
client_secret = None
|
||||
if client_metadata.token_endpoint_auth_method != "none":
|
||||
if client_metadata.token_endpoint_auth_method != "none": # pragma: no branch
|
||||
# cryptographically secure random 32-byte hex string
|
||||
client_secret = secrets.token_hex(32)
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ class RegistrationHandler:
|
|||
elif client_metadata.scope is not None and self.options.valid_scopes is not None:
|
||||
requested_scopes = set(client_metadata.scope.split())
|
||||
valid_scopes = set(self.options.valid_scopes)
|
||||
if not requested_scopes.issubset(valid_scopes):
|
||||
if not requested_scopes.issubset(valid_scopes): # pragma: no branch
|
||||
return PydanticJSONResponse(
|
||||
content=RegistrationErrorResponse(
|
||||
error="invalid_client_metadata",
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class RevocationHandler:
|
|||
client = await self.client_authenticator.authenticate(
|
||||
revocation_request.client_id, revocation_request.client_secret
|
||||
)
|
||||
except AuthenticationError as e:
|
||||
except AuthenticationError as e: # pragma: no cover
|
||||
return PydanticJSONResponse(
|
||||
status_code=401,
|
||||
content=RevocationErrorResponse(
|
||||
|
|
@ -69,7 +69,7 @@ class RevocationHandler:
|
|||
self.provider.load_access_token,
|
||||
partial(self.provider.load_refresh_token, client),
|
||||
]
|
||||
if revocation_request.token_type_hint == "refresh_token":
|
||||
if revocation_request.token_type_hint == "refresh_token": # pragma: no cover
|
||||
loaders = reversed(loaders)
|
||||
|
||||
token: None | AccessToken | RefreshToken = None
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class TokenHandler:
|
|||
client_id=token_request.client_id,
|
||||
client_secret=token_request.client_secret,
|
||||
)
|
||||
except AuthenticationError as e:
|
||||
except AuthenticationError as e: # pragma: no cover
|
||||
return self.response(
|
||||
TokenErrorResponse(
|
||||
error="unauthorized_client",
|
||||
|
|
@ -115,7 +115,7 @@ class TokenHandler:
|
|||
)
|
||||
)
|
||||
|
||||
if token_request.grant_type not in client_info.grant_types:
|
||||
if token_request.grant_type not in client_info.grant_types: # pragma: no cover
|
||||
return self.response(
|
||||
TokenErrorResponse(
|
||||
error="unsupported_grant_type",
|
||||
|
|
@ -151,7 +151,7 @@ class TokenHandler:
|
|||
# see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
|
||||
if auth_code.redirect_uri_provided_explicitly:
|
||||
authorize_request_redirect_uri = auth_code.redirect_uri
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
authorize_request_redirect_uri = None
|
||||
|
||||
# Convert both sides to strings for comparison to handle AnyUrl vs string issues
|
||||
|
|
@ -192,7 +192,7 @@ class TokenHandler:
|
|||
)
|
||||
)
|
||||
|
||||
case RefreshTokenRequest():
|
||||
case RefreshTokenRequest(): # pragma: no cover
|
||||
refresh_token = await self.provider.load_refresh_token(client_info, token_request.refresh_token)
|
||||
if refresh_token is None or refresh_token.client_id != token_request.client_id:
|
||||
# if token belongs to different client, pretend it doesn't exist
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class RequireAuthMiddleware:
|
|||
"""Send an authentication error response with WWW-Authenticate header."""
|
||||
# Build WWW-Authenticate header value
|
||||
www_auth_parts = [f'error="{error}"', f'error_description="{description}"']
|
||||
if self.resource_metadata_url:
|
||||
if self.resource_metadata_url: # pragma: no cover
|
||||
www_auth_parts.append(f'resource_metadata="{self.resource_metadata_url}"')
|
||||
|
||||
www_authenticate = f"Bearer {', '.join(www_auth_parts)}"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from mcp.shared.auth import OAuthClientInformationFull
|
|||
|
||||
class AuthenticationError(Exception):
|
||||
def __init__(self, message: str):
|
||||
self.message = message
|
||||
self.message = message # pragma: no cover
|
||||
|
||||
|
||||
class ClientAuthenticator:
|
||||
|
|
@ -34,18 +34,18 @@ class ClientAuthenticator:
|
|||
# Look up client information
|
||||
client = await self.provider.get_client(client_id)
|
||||
if not client:
|
||||
raise AuthenticationError("Invalid client_id")
|
||||
raise AuthenticationError("Invalid client_id") # pragma: no cover
|
||||
|
||||
# If client from the store expects a secret, validate that the request provides
|
||||
# that secret
|
||||
if client.client_secret:
|
||||
if client.client_secret: # pragma: no branch
|
||||
if not client_secret:
|
||||
raise AuthenticationError("Client secret is required")
|
||||
raise AuthenticationError("Client secret is required") # pragma: no cover
|
||||
|
||||
if client.client_secret != client_secret:
|
||||
raise AuthenticationError("Invalid client_secret")
|
||||
raise AuthenticationError("Invalid client_secret") # pragma: no cover
|
||||
|
||||
if client.client_secret_expires_at and client.client_secret_expires_at < int(time.time()):
|
||||
raise AuthenticationError("Client secret has expired")
|
||||
raise AuthenticationError("Client secret has expired") # pragma: no cover
|
||||
|
||||
return client
|
||||
|
|
|
|||
|
|
@ -117,7 +117,6 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref
|
|||
Returns:
|
||||
The client information, or None if the client does not exist.
|
||||
"""
|
||||
...
|
||||
|
||||
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
|
||||
"""
|
||||
|
|
@ -132,7 +131,6 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref
|
|||
Raises:
|
||||
RegistrationError: If the client metadata is invalid.
|
||||
"""
|
||||
...
|
||||
|
||||
async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
|
||||
"""
|
||||
|
|
@ -221,8 +219,7 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref
|
|||
Returns:
|
||||
The RefreshToken object if found, or None if not found.
|
||||
"""
|
||||
|
||||
...
|
||||
...
|
||||
|
||||
async def exchange_refresh_token(
|
||||
self,
|
||||
|
|
@ -258,7 +255,6 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref
|
|||
Returns:
|
||||
The AuthInfo, or None if the token is invalid.
|
||||
"""
|
||||
...
|
||||
|
||||
async def revoke_token(
|
||||
self,
|
||||
|
|
@ -276,7 +272,6 @@ class OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, Ref
|
|||
Args:
|
||||
token: the token to revoke
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
def construct_redirect_uri(redirect_uri_base: str, **params: str | None) -> str:
|
||||
|
|
|
|||
|
|
@ -38,13 +38,13 @@ def validate_issuer_url(url: AnyHttpUrl):
|
|||
and url.host != "localhost"
|
||||
and (url.host is not None and not url.host.startswith("127.0.0.1"))
|
||||
):
|
||||
raise ValueError("Issuer URL must be HTTPS")
|
||||
raise ValueError("Issuer URL must be HTTPS") # pragma: no cover
|
||||
|
||||
# No fragments or query parameters allowed
|
||||
if url.fragment:
|
||||
raise ValueError("Issuer URL must not have a fragment")
|
||||
raise ValueError("Issuer URL must not have a fragment") # pragma: no cover
|
||||
if url.query:
|
||||
raise ValueError("Issuer URL must not have a query string")
|
||||
raise ValueError("Issuer URL must not have a query string") # pragma: no cover
|
||||
|
||||
|
||||
AUTHORIZATION_PATH = "/authorize"
|
||||
|
|
@ -115,7 +115,7 @@ def create_auth_routes(
|
|||
),
|
||||
]
|
||||
|
||||
if client_registration_options.enabled:
|
||||
if client_registration_options.enabled: # pragma: no branch
|
||||
registration_handler = RegistrationHandler(
|
||||
provider,
|
||||
options=client_registration_options,
|
||||
|
|
@ -131,7 +131,7 @@ def create_auth_routes(
|
|||
)
|
||||
)
|
||||
|
||||
if revocation_options.enabled:
|
||||
if revocation_options.enabled: # pragma: no branch
|
||||
revocation_handler = RevocationHandler(provider, client_authenticator)
|
||||
routes.append(
|
||||
Route(
|
||||
|
|
@ -176,11 +176,11 @@ def build_metadata(
|
|||
)
|
||||
|
||||
# Add registration endpoint if supported
|
||||
if client_registration_options.enabled:
|
||||
if client_registration_options.enabled: # pragma: no branch
|
||||
metadata.registration_endpoint = AnyHttpUrl(str(issuer_url).rstrip("/") + REGISTRATION_PATH)
|
||||
|
||||
# Add revocation endpoint if supported
|
||||
if revocation_options.enabled:
|
||||
if revocation_options.enabled: # pragma: no branch
|
||||
metadata.revocation_endpoint = AnyHttpUrl(str(issuer_url).rstrip("/") + REVOCATION_PATH)
|
||||
metadata.revocation_endpoint_auth_methods_supported = ["client_secret_post"]
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ def _is_primitive_field(field_info: FieldInfo) -> bool:
|
|||
annotation = field_info.annotation
|
||||
|
||||
# Handle None type
|
||||
if annotation is types.NoneType:
|
||||
if annotation is types.NoneType: # pragma: no cover
|
||||
return True
|
||||
|
||||
# Handle basic primitive types
|
||||
|
|
@ -104,8 +104,8 @@ async def elicit_with_validation(
|
|||
return AcceptedElicitation(data=validated_data)
|
||||
elif result.action == "decline":
|
||||
return DeclinedElicitation()
|
||||
elif result.action == "cancel":
|
||||
elif result.action == "cancel": # pragma: no cover
|
||||
return CancelledElicitation()
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
# This should never happen, but handle it just in case
|
||||
raise ValueError(f"Unexpected elicitation action: {result.action}")
|
||||
|
|
|
|||
|
|
@ -94,11 +94,11 @@ class Prompt(BaseModel):
|
|||
"""
|
||||
func_name = name or fn.__name__
|
||||
|
||||
if func_name == "<lambda>":
|
||||
if func_name == "<lambda>": # pragma: no cover
|
||||
raise ValueError("You must provide a name for lambda functions")
|
||||
|
||||
# Find context parameter if it exists
|
||||
if context_kwarg is None:
|
||||
if context_kwarg is None: # pragma: no branch
|
||||
context_kwarg = find_context_parameter(fn)
|
||||
|
||||
# Get schema from func_metadata, excluding context parameter
|
||||
|
|
@ -110,7 +110,7 @@ class Prompt(BaseModel):
|
|||
|
||||
# Convert parameters to PromptArguments
|
||||
arguments: list[PromptArgument] = []
|
||||
if "properties" in parameters:
|
||||
if "properties" in parameters: # pragma: no branch
|
||||
for param_name, param in parameters["properties"].items():
|
||||
required = param_name in parameters.get("required", [])
|
||||
arguments.append(
|
||||
|
|
@ -172,12 +172,12 @@ class Prompt(BaseModel):
|
|||
elif isinstance(msg, str):
|
||||
content = TextContent(type="text", text=msg)
|
||||
messages.append(UserMessage(content=content))
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
content = pydantic_core.to_json(msg, fallback=str, indent=2).decode()
|
||||
messages.append(Message(role="user", content=content))
|
||||
except Exception:
|
||||
except Exception: # pragma: no cover
|
||||
raise ValueError(f"Could not convert prompt result to message: {msg}")
|
||||
|
||||
return messages
|
||||
except Exception as e:
|
||||
except Exception as e: # pragma: no cover
|
||||
raise ValueError(f"Error rendering prompt {self.name}: {e}")
|
||||
|
|
|
|||
|
|
@ -46,4 +46,4 @@ class Resource(BaseModel, abc.ABC):
|
|||
@abc.abstractmethod
|
||||
async def read(self) -> str | bytes:
|
||||
"""Read the resource content."""
|
||||
pass
|
||||
pass # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ class ResourceManager:
|
|||
if params := template.matches(uri_str):
|
||||
try:
|
||||
return await template.create_resource(uri_str, params, context=context)
|
||||
except Exception as e:
|
||||
except Exception as e: # pragma: no cover
|
||||
raise ValueError(f"Error creating resource from template: {e}")
|
||||
|
||||
raise ValueError(f"Unknown resource: {uri}")
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ class ResourceTemplate(BaseModel):
|
|||
"""Create a template from a function."""
|
||||
func_name = name or fn.__name__
|
||||
if func_name == "<lambda>":
|
||||
raise ValueError("You must provide a name for lambda functions")
|
||||
raise ValueError("You must provide a name for lambda functions") # pragma: no cover
|
||||
|
||||
# Find context parameter if it exists
|
||||
if context_kwarg is None:
|
||||
if context_kwarg is None: # pragma: no branch
|
||||
context_kwarg = find_context_parameter(fn)
|
||||
|
||||
# Get schema from func_metadata, excluding context parameter
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class TextResource(Resource):
|
|||
|
||||
async def read(self) -> str:
|
||||
"""Read the text content."""
|
||||
return self.text
|
||||
return self.text # pragma: no cover
|
||||
|
||||
|
||||
class BinaryResource(Resource):
|
||||
|
|
@ -34,7 +34,7 @@ class BinaryResource(Resource):
|
|||
|
||||
async def read(self) -> bytes:
|
||||
"""Read the binary content."""
|
||||
return self.data
|
||||
return self.data # pragma: no cover
|
||||
|
||||
|
||||
class FunctionResource(Resource):
|
||||
|
|
@ -61,7 +61,7 @@ class FunctionResource(Resource):
|
|||
if inspect.iscoroutine(result):
|
||||
result = await result
|
||||
|
||||
if isinstance(result, Resource):
|
||||
if isinstance(result, Resource): # pragma: no cover
|
||||
return await result.read()
|
||||
elif isinstance(result, bytes):
|
||||
return result
|
||||
|
|
@ -86,7 +86,7 @@ class FunctionResource(Resource):
|
|||
) -> "FunctionResource":
|
||||
"""Create a FunctionResource from a function."""
|
||||
func_name = name or fn.__name__
|
||||
if func_name == "<lambda>":
|
||||
if func_name == "<lambda>": # pragma: no cover
|
||||
raise ValueError("You must provide a name for lambda functions")
|
||||
|
||||
# ensure the arguments are properly cast
|
||||
|
|
@ -122,7 +122,7 @@ class FileResource(Resource):
|
|||
|
||||
@pydantic.field_validator("path")
|
||||
@classmethod
|
||||
def validate_absolute_path(cls, path: Path) -> Path:
|
||||
def validate_absolute_path(cls, path: Path) -> Path: # pragma: no cover
|
||||
"""Ensure path is absolute."""
|
||||
if not path.is_absolute():
|
||||
raise ValueError("Path must be absolute")
|
||||
|
|
@ -155,7 +155,7 @@ class HttpResource(Resource):
|
|||
|
||||
async def read(self) -> str | bytes:
|
||||
"""Read the HTTP content."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient() as client: # pragma: no cover
|
||||
response = await client.get(self.url)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
|
@ -171,13 +171,13 @@ class DirectoryResource(Resource):
|
|||
|
||||
@pydantic.field_validator("path")
|
||||
@classmethod
|
||||
def validate_absolute_path(cls, path: Path) -> Path:
|
||||
def validate_absolute_path(cls, path: Path) -> Path: # pragma: no cover
|
||||
"""Ensure path is absolute."""
|
||||
if not path.is_absolute():
|
||||
raise ValueError("Path must be absolute")
|
||||
return path
|
||||
|
||||
def list_files(self) -> list[Path]:
|
||||
def list_files(self) -> list[Path]: # pragma: no cover
|
||||
"""List files in the directory."""
|
||||
if not self.path.exists():
|
||||
raise FileNotFoundError(f"Directory not found: {self.path}")
|
||||
|
|
@ -191,7 +191,7 @@ class DirectoryResource(Resource):
|
|||
except Exception as e:
|
||||
raise ValueError(f"Error listing directory {self.path}: {e}")
|
||||
|
||||
async def read(self) -> str: # Always returns JSON string
|
||||
async def read(self) -> str: # Always returns JSON string # pragma: no cover
|
||||
"""Read the directory listing."""
|
||||
try:
|
||||
files = await anyio.to_thread.run_sync(self.list_files)
|
||||
|
|
|
|||
|
|
@ -203,18 +203,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)
|
||||
# Validate auth configuration
|
||||
if self.settings.auth is not None:
|
||||
if auth_server_provider and token_verifier:
|
||||
if auth_server_provider and token_verifier: # pragma: no cover
|
||||
raise ValueError("Cannot specify both auth_server_provider and token_verifier")
|
||||
if not auth_server_provider and not token_verifier:
|
||||
if not auth_server_provider and not token_verifier: # pragma: no cover
|
||||
raise ValueError("Must specify either auth_server_provider or token_verifier when auth is enabled")
|
||||
elif auth_server_provider or token_verifier:
|
||||
elif auth_server_provider or token_verifier: # pragma: no cover
|
||||
raise ValueError("Cannot specify auth_server_provider or token_verifier without auth settings")
|
||||
|
||||
self._auth_server_provider = auth_server_provider
|
||||
self._token_verifier = token_verifier
|
||||
|
||||
# Create token verifier from provider if needed (backwards compatibility)
|
||||
if auth_server_provider and not token_verifier:
|
||||
if auth_server_provider and not token_verifier: # pragma: no cover
|
||||
self._token_verifier = ProviderTokenVerifier(auth_server_provider)
|
||||
self._event_store = event_store
|
||||
self._custom_starlette_routes: list[Route] = []
|
||||
|
|
@ -253,14 +253,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
Raises:
|
||||
RuntimeError: If called before streamable_http_app() has been called.
|
||||
"""
|
||||
if self._session_manager is None:
|
||||
if self._session_manager is None: # pragma: no cover
|
||||
raise RuntimeError(
|
||||
"Session manager can only be accessed after"
|
||||
"calling streamable_http_app()."
|
||||
"The session manager is created lazily"
|
||||
"to avoid unnecessary initialization."
|
||||
)
|
||||
return self._session_manager
|
||||
return self._session_manager # pragma: no cover
|
||||
|
||||
def run(
|
||||
self,
|
||||
|
|
@ -274,15 +274,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
mount_path: Optional mount path for SSE transport
|
||||
"""
|
||||
TRANSPORTS = Literal["stdio", "sse", "streamable-http"]
|
||||
if transport not in TRANSPORTS.__args__: # type: ignore
|
||||
if transport not in TRANSPORTS.__args__: # type: ignore # pragma: no cover
|
||||
raise ValueError(f"Unknown transport: {transport}")
|
||||
|
||||
match transport:
|
||||
case "stdio":
|
||||
anyio.run(self.run_stdio_async)
|
||||
case "sse":
|
||||
case "sse": # pragma: no cover
|
||||
anyio.run(lambda: self.run_sse_async(mount_path))
|
||||
case "streamable-http":
|
||||
case "streamable-http": # pragma: no cover
|
||||
anyio.run(self.run_streamable_http_async)
|
||||
|
||||
def _setup_handlers(self) -> None:
|
||||
|
|
@ -368,13 +368,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
|
||||
context = self.get_context()
|
||||
resource = await self._resource_manager.get_resource(uri, context=context)
|
||||
if not resource:
|
||||
if not resource: # pragma: no cover
|
||||
raise ResourceError(f"Unknown resource: {uri}")
|
||||
|
||||
try:
|
||||
content = await resource.read()
|
||||
return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
|
||||
except Exception as e:
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.exception(f"Error reading resource {uri}")
|
||||
raise ResourceError(str(e))
|
||||
|
||||
|
|
@ -710,7 +710,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
return JSONResponse({"status": "ok"})
|
||||
"""
|
||||
|
||||
def decorator(
|
||||
def decorator( # pragma: no cover
|
||||
func: Callable[[Request], Awaitable[Response]],
|
||||
) -> Callable[[Request], Awaitable[Response]]:
|
||||
self._custom_starlette_routes.append(
|
||||
|
|
@ -724,7 +724,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
return decorator # pragma: no cover
|
||||
|
||||
async def run_stdio_async(self) -> None:
|
||||
"""Run the server using stdio transport."""
|
||||
|
|
@ -735,7 +735,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
self._mcp_server.create_initialization_options(),
|
||||
)
|
||||
|
||||
async def run_sse_async(self, mount_path: str | None = None) -> None:
|
||||
async def run_sse_async(self, mount_path: str | None = None) -> None: # pragma: no cover
|
||||
"""Run the server using SSE transport."""
|
||||
import uvicorn
|
||||
|
||||
|
|
@ -750,7 +750,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
server = uvicorn.Server(config)
|
||||
await server.serve()
|
||||
|
||||
async def run_streamable_http_async(self) -> None:
|
||||
async def run_streamable_http_async(self) -> None: # pragma: no cover
|
||||
"""Run the server using StreamableHTTP transport."""
|
||||
import uvicorn
|
||||
|
||||
|
|
@ -810,7 +810,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
security_settings=self.settings.transport_security,
|
||||
)
|
||||
|
||||
async def handle_sse(scope: Scope, receive: Receive, send: Send):
|
||||
async def handle_sse(scope: Scope, receive: Receive, send: Send): # pragma: no cover
|
||||
# Add client ID from auth context into request context if available
|
||||
|
||||
async with sse.connect_sse(
|
||||
|
|
@ -831,7 +831,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
required_scopes = []
|
||||
|
||||
# Set up auth if configured
|
||||
if self.settings.auth:
|
||||
if self.settings.auth: # pragma: no cover
|
||||
required_scopes = self.settings.auth.required_scopes or []
|
||||
|
||||
# Add auth middleware if token verifier is available
|
||||
|
|
@ -862,7 +862,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
|
||||
# When auth is configured, require authentication
|
||||
if self._token_verifier:
|
||||
if self._token_verifier: # pragma: no cover
|
||||
# Determine resource metadata URL
|
||||
resource_metadata_url = None
|
||||
if self.settings.auth and self.settings.auth.resource_server_url:
|
||||
|
|
@ -885,7 +885,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
app=RequireAuthMiddleware(sse.handle_post_message, required_scopes, resource_metadata_url),
|
||||
)
|
||||
)
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
# Auth is disabled, no need for RequireAuthMiddleware
|
||||
# Since handle_sse is an ASGI app, we need to create a compatible endpoint
|
||||
async def sse_endpoint(request: Request) -> Response:
|
||||
|
|
@ -906,7 +906,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
)
|
||||
# Add protected resource metadata endpoint if configured as RS
|
||||
if self.settings.auth and self.settings.auth.resource_server_url:
|
||||
if self.settings.auth and self.settings.auth.resource_server_url: # pragma: no cover
|
||||
from mcp.server.auth.routes import create_protected_resource_routes
|
||||
|
||||
routes.extend(
|
||||
|
|
@ -928,7 +928,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
from starlette.middleware import Middleware
|
||||
|
||||
# Create session manager on first call (lazy initialization)
|
||||
if self._session_manager is None:
|
||||
if self._session_manager is None: # pragma: no branch
|
||||
self._session_manager = StreamableHTTPSessionManager(
|
||||
app=self._mcp_server,
|
||||
event_store=self._event_store,
|
||||
|
|
@ -946,7 +946,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
required_scopes = []
|
||||
|
||||
# Set up auth if configured
|
||||
if self.settings.auth:
|
||||
if self.settings.auth: # pragma: no cover
|
||||
required_scopes = self.settings.auth.required_scopes or []
|
||||
|
||||
# Add auth middleware if token verifier is available
|
||||
|
|
@ -974,7 +974,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
|
||||
# Set up routes with or without auth
|
||||
if self._token_verifier:
|
||||
if self._token_verifier: # pragma: no cover
|
||||
# Determine resource metadata URL
|
||||
resource_metadata_url = None
|
||||
if self.settings.auth and self.settings.auth.resource_server_url:
|
||||
|
|
@ -999,7 +999,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
|
||||
# Add protected resource metadata endpoint if configured as RS
|
||||
if self.settings.auth and self.settings.auth.resource_server_url:
|
||||
if self.settings.auth and self.settings.auth.resource_server_url: # pragma: no cover
|
||||
from mcp.server.auth.routes import create_protected_resource_routes
|
||||
|
||||
routes.extend(
|
||||
|
|
@ -1066,7 +1066,7 @@ class StreamableHTTPASGIApp:
|
|||
def __init__(self, session_manager: StreamableHTTPSessionManager):
|
||||
self.session_manager = session_manager
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # pragma: no cover
|
||||
await self.session_manager.handle_request(scope, receive, send)
|
||||
|
||||
|
||||
|
|
@ -1121,16 +1121,16 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
|
|||
@property
|
||||
def fastmcp(self) -> FastMCP:
|
||||
"""Access to the FastMCP server."""
|
||||
if self._fastmcp is None:
|
||||
if self._fastmcp is None: # pragma: no cover
|
||||
raise ValueError("Context is not available outside of a request")
|
||||
return self._fastmcp
|
||||
return self._fastmcp # pragma: no cover
|
||||
|
||||
@property
|
||||
def request_context(
|
||||
self,
|
||||
) -> RequestContext[ServerSessionT, LifespanContextT, RequestT]:
|
||||
"""Access to the underlying request context."""
|
||||
if self._request_context is None:
|
||||
if self._request_context is None: # pragma: no cover
|
||||
raise ValueError("Context is not available outside of a request")
|
||||
return self._request_context
|
||||
|
||||
|
|
@ -1144,7 +1144,7 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
|
|||
"""
|
||||
progress_token = self.request_context.meta.progressToken if self.request_context.meta else None
|
||||
|
||||
if progress_token is None:
|
||||
if progress_token is None: # pragma: no cover
|
||||
return
|
||||
|
||||
await self.request_context.session.send_progress_notification(
|
||||
|
|
@ -1225,7 +1225,9 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT, RequestT]):
|
|||
@property
|
||||
def client_id(self) -> str | None:
|
||||
"""Get the client ID if available."""
|
||||
return getattr(self.request_context.meta, "client_id", None) if self.request_context.meta else None
|
||||
return (
|
||||
getattr(self.request_context.meta, "client_id", None) if self.request_context.meta else None
|
||||
) # pragma: no cover
|
||||
|
||||
@property
|
||||
def request_id(self) -> str:
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class Tool(BaseModel):
|
|||
func_doc = description or fn.__doc__ or ""
|
||||
is_async = _is_async_callable(fn)
|
||||
|
||||
if context_kwarg is None:
|
||||
if context_kwarg is None: # pragma: no branch
|
||||
context_kwarg = find_context_parameter(fn)
|
||||
|
||||
func_arg_metadata = func_metadata(
|
||||
|
|
@ -110,7 +110,7 @@ class Tool(BaseModel):
|
|||
|
||||
|
||||
def _is_async_callable(obj: Any) -> bool:
|
||||
while isinstance(obj, functools.partial):
|
||||
while isinstance(obj, functools.partial): # pragma: no cover
|
||||
obj = obj.func
|
||||
|
||||
return inspect.iscoroutinefunction(obj) or (
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ class FuncMetadata(BaseModel):
|
|||
key_to_field_info[field_info.alias] = field_info
|
||||
|
||||
for data_key, data_value in data.items():
|
||||
if data_key not in key_to_field_info:
|
||||
if data_key not in key_to_field_info: # pragma: no cover
|
||||
continue
|
||||
|
||||
field_info = key_to_field_info[data_key]
|
||||
|
|
@ -217,7 +217,7 @@ def func_metadata(
|
|||
dynamic_pydantic_model_params: dict[str, Any] = {}
|
||||
globalns = getattr(func, "__globals__", {})
|
||||
for param in params.values():
|
||||
if param.name.startswith("_"):
|
||||
if param.name.startswith("_"): # pragma: no cover
|
||||
raise InvalidSignature(f"Parameter {param.name} of {func.__name__} cannot start with '_'")
|
||||
if param.name in skip_names:
|
||||
continue
|
||||
|
|
@ -412,7 +412,7 @@ def _create_model_from_class(cls: type[Any]) -> type[BaseModel]:
|
|||
|
||||
model_fields: dict[str, Any] = {}
|
||||
for field_name, field_type in type_hints.items():
|
||||
if field_name.startswith("_"):
|
||||
if field_name.startswith("_"): # pragma: no cover
|
||||
continue
|
||||
|
||||
default = getattr(cls, field_name, PydanticUndefined)
|
||||
|
|
@ -477,7 +477,9 @@ def _create_dict_model(func_name: str, dict_annotation: Any) -> type[BaseModel]:
|
|||
|
||||
|
||||
def _get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
|
||||
def try_eval_type(value: Any, globalns: dict[str, Any], localns: dict[str, Any]) -> tuple[Any, bool]:
|
||||
def try_eval_type(
|
||||
value: Any, globalns: dict[str, Any], localns: dict[str, Any]
|
||||
) -> tuple[Any, bool]: # pragma: no cover
|
||||
try:
|
||||
return eval_type_backport(value, globalns, localns), True
|
||||
except NameError:
|
||||
|
|
@ -489,7 +491,7 @@ def _get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
|
|||
|
||||
# This check and raise could perhaps be skipped, and we (FastMCP) just call
|
||||
# model_rebuild right before using it 🤷
|
||||
if status is False:
|
||||
if status is False: # pragma: no cover
|
||||
raise InvalidSignature(f"Unable to evaluate type annotation {annotation}")
|
||||
|
||||
return annotation
|
||||
|
|
@ -524,7 +526,7 @@ def _convert_to_content(
|
|||
output than the lowlevel server tool call handler, which just serializes structured
|
||||
content verbatim.
|
||||
"""
|
||||
if result is None:
|
||||
if result is None: # pragma: no cover
|
||||
return []
|
||||
|
||||
if isinstance(result, ContentBlock):
|
||||
|
|
|
|||
|
|
@ -25,15 +25,15 @@ def configure_logging(
|
|||
level: the log level to use
|
||||
"""
|
||||
handlers: list[logging.Handler] = []
|
||||
try:
|
||||
try: # pragma: no cover
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
|
||||
handlers.append(RichHandler(console=Console(stderr=True), rich_tracebacks=True))
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
if not handlers:
|
||||
if not handlers: # pragma: no cover
|
||||
handlers.append(logging.StreamHandler())
|
||||
|
||||
logging.basicConfig(
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ class Image:
|
|||
data: bytes | None = None,
|
||||
format: str | None = None,
|
||||
):
|
||||
if path is None and data is None:
|
||||
if path is None and data is None: # pragma: no cover
|
||||
raise ValueError("Either path or data must be provided")
|
||||
if path is not None and data is not None:
|
||||
if path is not None and data is not None: # pragma: no cover
|
||||
raise ValueError("Only one of path or data can be provided")
|
||||
|
||||
self.path = Path(path) if path else None
|
||||
|
|
@ -27,7 +27,7 @@ class Image:
|
|||
|
||||
def _get_mime_type(self) -> str:
|
||||
"""Get MIME type from format or guess from file extension."""
|
||||
if self._format:
|
||||
if self._format: # pragma: no cover
|
||||
return f"image/{self._format.lower()}"
|
||||
|
||||
if self.path:
|
||||
|
|
@ -39,16 +39,16 @@ class Image:
|
|||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
}.get(suffix, "application/octet-stream")
|
||||
return "image/png" # default for raw binary data
|
||||
return "image/png" # pragma: no cover # default for raw binary data
|
||||
|
||||
def to_image_content(self) -> ImageContent:
|
||||
"""Convert to MCP ImageContent."""
|
||||
if self.path:
|
||||
with open(self.path, "rb") as f:
|
||||
data = base64.b64encode(f.read()).decode()
|
||||
elif self.data is not None:
|
||||
elif self.data is not None: # pragma: no cover
|
||||
data = base64.b64encode(self.data).decode()
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
raise ValueError("No image data available")
|
||||
|
||||
return ImageContent(type="image", data=data, mimeType=self._mime_type)
|
||||
|
|
@ -63,7 +63,7 @@ class Audio:
|
|||
data: bytes | None = None,
|
||||
format: str | None = None,
|
||||
):
|
||||
if not bool(path) ^ bool(data):
|
||||
if not bool(path) ^ bool(data): # pragma: no cover
|
||||
raise ValueError("Either path or data can be provided")
|
||||
|
||||
self.path = Path(path) if path else None
|
||||
|
|
@ -73,7 +73,7 @@ class Audio:
|
|||
|
||||
def _get_mime_type(self) -> str:
|
||||
"""Get MIME type from format or guess from file extension."""
|
||||
if self._format:
|
||||
if self._format: # pragma: no cover
|
||||
return f"audio/{self._format.lower()}"
|
||||
|
||||
if self.path:
|
||||
|
|
@ -86,16 +86,16 @@ class Audio:
|
|||
".aac": "audio/aac",
|
||||
".m4a": "audio/mp4",
|
||||
}.get(suffix, "application/octet-stream")
|
||||
return "audio/wav" # default for raw binary data
|
||||
return "audio/wav" # pragma: no cover # default for raw binary data
|
||||
|
||||
def to_audio_content(self) -> AudioContent:
|
||||
"""Convert to MCP AudioContent."""
|
||||
if self.path:
|
||||
with open(self.path, "rb") as f:
|
||||
data = base64.b64encode(f.read()).decode()
|
||||
elif self.data is not None:
|
||||
elif self.data is not None: # pragma: no cover
|
||||
data = base64.b64encode(self.data).decode()
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
raise ValueError("No audio data available")
|
||||
|
||||
return AudioContent(type="audio", data=data, mimeType=self._mime_type)
|
||||
|
|
|
|||
|
|
@ -20,27 +20,27 @@ def create_call_wrapper(func: Callable[..., R], request_type: type[T]) -> Callab
|
|||
try:
|
||||
sig = inspect.signature(func)
|
||||
type_hints = get_type_hints(func)
|
||||
except (ValueError, TypeError, NameError):
|
||||
except (ValueError, TypeError, NameError): # pragma: no cover
|
||||
return lambda _: func()
|
||||
|
||||
# Check for positional-only parameter typed as request_type
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param.kind == inspect.Parameter.POSITIONAL_ONLY:
|
||||
param_type = type_hints.get(param_name)
|
||||
if param_type == request_type:
|
||||
if param_type == request_type: # pragma: no branch
|
||||
# Check if it has a default - if so, treat as old style
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
if param.default is not inspect.Parameter.empty: # pragma: no cover
|
||||
return lambda _: func()
|
||||
# Found positional-only parameter with correct type and no default
|
||||
return lambda req: func(req)
|
||||
|
||||
# Check for any positional/keyword parameter typed as request_type
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
|
||||
if param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY): # pragma: no branch
|
||||
param_type = type_hints.get(param_name)
|
||||
if param_type == request_type:
|
||||
# Check if it has a default - if so, treat as old style
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
if param.default is not inspect.Parameter.empty: # pragma: no cover
|
||||
return lambda _: func()
|
||||
|
||||
# Found keyword parameter with correct type and no default
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue