diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml deleted file mode 100644 index 577f6cc11..000000000 --- a/.github/workflows/run-integration-tests.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Run integration tests - -env: - # enable colored output - PY_COLORS: 1 - -on: - push: - branches: ["main"] - paths: - - "src/**" - - "integration_tests/**" - - "uv.lock" - - "pyproject.toml" - - ".github/workflows/**" - - # run on all pull requests because these checks are required and will block merges otherwise - pull_request: - - workflow_dispatch: - -permissions: - contents: read - -jobs: - run_tests: - name: "Run tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}" - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - python-version: ["3.10"] - fail-fast: false - timeout-minutes: 5 - - steps: - - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - python-version: ${{ matrix.python-version }} - - - name: Install FastMCP - run: uv sync --locked - - - name: Run tests - run: uv run pytest integration_tests diff --git a/integration_tests/test_repro_518_output.py b/integration_tests/test_repro_518_output.py deleted file mode 100644 index b82dd03ae..000000000 --- a/integration_tests/test_repro_518_output.py +++ /dev/null @@ -1,89 +0,0 @@ -import os -import subprocess -import sys -import tempfile -import time - -import httpx -import pytest - -PYTHON_EXE = sys.executable - -SERVER_CODE = """ -import uvicorn -from fastapi import FastAPI -from fastmcp import FastMCP - -mcp = FastMCP() - -@mcp.tool("dummy_tool", "A simple dummy tool for the test server") -def add(a: int, b: int) -> int: - return a + b - -app = FastAPI() # Intentionally no lifespan=mcp.lifespan -app.mount("/", mcp.http_app(transport="streamable-http")) - -if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8080, log_config=None) -""" - - -@pytest.mark.timeout(20) -def test_server_shows_informative_error_on_stderr(): - """ - Runs a minimal FastMCP+FastAPI server (that omits lifespan wiring) - as a subprocess, triggers the error via an HTTP request, and then checks - if the server's stderr contains the specific informative error message for issue #518. - """ - process = None - captured_stderr = "" - - with tempfile.NamedTemporaryFile( - mode="w", suffix=".py", delete=False - ) as tmp_script: - tmp_script.write(SERVER_CODE) - tmp_script_path = tmp_script.name - - try: - process = subprocess.Popen( - [PYTHON_EXE, "-u", tmp_script_path], - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - text=True, - universal_newlines=True, - ) - - time.sleep(3) - - if process.poll() is None: - try: - with httpx.Client(timeout=5.0) as client: - # The mounted FastMCP app is at root, its internal default path is /mcp - client.get("http://localhost:8080/mcp/") - except httpx.RequestError: - pass - time.sleep(1) - - finally: - if process: - if process.poll() is None: - process.terminate() - try: - _, captured_stderr = process.communicate(timeout=10) - except subprocess.TimeoutExpired: - process.kill() - _, captured_stderr = process.communicate() - - if os.path.exists(tmp_script_path): - os.unlink(tmp_script_path) - - assert captured_stderr is not None, "stderr should have been captured" - normalized_stderr = captured_stderr.replace("\r\n", "\n").replace("\r", "\n") - - assert ( - "FastMCP's StreamableHTTPSessionManager task group was not initialized" - in normalized_stderr - ) - assert "lifespan=mcp_app.lifespan" in normalized_stderr - assert "gofastmcp.com/deployment/asgi" in normalized_stderr - assert "Original error: Task group is not initialized" in normalized_stderr diff --git a/src/fastmcp/utilities/tests.py b/src/fastmcp/utilities/tests.py index 39bd6b9c7..0d64a1292 100644 --- a/src/fastmcp/utilities/tests.py +++ b/src/fastmcp/utilities/tests.py @@ -71,7 +71,7 @@ def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> No @contextmanager def run_server_in_process( - server_fn: Callable[[str, int], None], *args + server_fn: Callable[..., None], *args ) -> Generator[str, None, None]: """ Context manager that runs a Starlette app in a separate process and returns the @@ -109,7 +109,11 @@ def run_server_in_process( yield f"http://{host}:{port}" - proc.kill() - proc.join(timeout=2) + proc.terminate() + proc.join(timeout=5) if proc.is_alive(): - raise RuntimeError("Server process failed to terminate") + # If it's still alive, then force kill it + proc.kill() + proc.join(timeout=2) + if proc.is_alive(): + raise RuntimeError("Server process failed to terminate even after kill") diff --git a/tests/contrib/test_bulk_tool_caller.py b/tests/contrib/test_bulk_tool_caller.py index 855341912..b6f86c927 100644 --- a/tests/contrib/test_bulk_tool_caller.py +++ b/tests/contrib/test_bulk_tool_caller.py @@ -85,7 +85,6 @@ ERROR_TOOL_NAME = "error_tool" NO_RETURN_TOOL_NAME = "no_return_tool" -@pytest.mark.asyncio async def test_call_tool_bulk_single_success(bulk_caller_live: BulkToolCaller): """Test single successful call via call_tool_bulk using echo_tool.""" tool_arguments = [{"arg1": "value1"}] @@ -98,7 +97,6 @@ async def test_call_tool_bulk_single_success(bulk_caller_live: BulkToolCaller): assert result == expected_result -@pytest.mark.asyncio async def test_call_tool_bulk_multiple_success(bulk_caller_live: BulkToolCaller): """Test multiple successful calls via call_tool_bulk using echo_tool.""" tool_arguments = [{"arg1": "value1"}, {"arg1": "value2"}] @@ -110,7 +108,6 @@ async def test_call_tool_bulk_multiple_success(bulk_caller_live: BulkToolCaller) assert results == expected_results -@pytest.mark.asyncio async def test_call_tool_bulk_error_stops(bulk_caller_live: BulkToolCaller): """Test call_tool_bulk stops on first error using error_tool.""" tool_arguments = [{"arg1": "error_value"}, {"arg1": "value2"}] @@ -125,7 +122,6 @@ async def test_call_tool_bulk_error_stops(bulk_caller_live: BulkToolCaller): assert result == expected_result -@pytest.mark.asyncio async def test_call_tool_bulk_error_continues(bulk_caller_live: BulkToolCaller): """Test call_tool_bulk continues on error using error_tool and echo_tool.""" tool_arguments = [{"arg1": "error_value"}, {"arg1": "success_value"}] @@ -148,7 +144,6 @@ async def test_call_tool_bulk_error_continues(bulk_caller_live: BulkToolCaller): assert success_result == expected_success_result -@pytest.mark.asyncio async def test_call_tools_bulk_single_success(bulk_caller_live: BulkToolCaller): """Test single successful call via call_tools_bulk using echo_tool.""" tool_calls = [CallToolRequest(tool=ECHO_TOOL_NAME, arguments={"arg1": "value1"})] @@ -161,7 +156,6 @@ async def test_call_tools_bulk_single_success(bulk_caller_live: BulkToolCaller): assert result == expected_result -@pytest.mark.asyncio async def test_call_tools_bulk_multiple_success(bulk_caller_live: BulkToolCaller): """Test multiple successful calls via call_tools_bulk with different tools.""" tool_calls = [ @@ -181,7 +175,6 @@ async def test_call_tools_bulk_multiple_success(bulk_caller_live: BulkToolCaller assert results == expected_results -@pytest.mark.asyncio async def test_call_tools_bulk_error_stops(bulk_caller_live: BulkToolCaller): """Test call_tools_bulk stops on first error using error_tool.""" tool_calls = [ @@ -199,7 +192,6 @@ async def test_call_tools_bulk_error_stops(bulk_caller_live: BulkToolCaller): assert result == expected_result -@pytest.mark.asyncio async def test_call_tools_bulk_error_continues(bulk_caller_live: BulkToolCaller): """Test call_tools_bulk continues on error using error_tool and echo_tool.""" tool_calls = [ diff --git a/tests/server/test_auth_integration.py b/tests/server/test_auth_integration.py index fb0330dcf..f64bd0843 100644 --- a/tests/server/test_auth_integration.py +++ b/tests/server/test_auth_integration.py @@ -341,7 +341,6 @@ async def tokens(test_client, registered_client, auth_code, pkce_challenge, requ class TestAuthEndpoints: - @pytest.mark.anyio async def test_metadata_endpoint(self, test_client: httpx.AsyncClient): """Test the OAuth 2.0 metadata endpoint.""" print("Sending request to metadata endpoint") @@ -370,7 +369,6 @@ class TestAuthEndpoints: ] assert metadata["service_documentation"] == "https://docs.example.com/" - @pytest.mark.anyio async def test_token_validation_error(self, test_client: httpx.AsyncClient): """Test token endpoint error - validation error.""" # Missing required fields @@ -387,7 +385,6 @@ class TestAuthEndpoints: "error_description" in error_response ) # Contains validation error messages - @pytest.mark.anyio async def test_token_invalid_auth_code( self, test_client, registered_client, pkce_challenge ): @@ -414,7 +411,6 @@ class TestAuthEndpoints: "authorization code does not exist" in error_response["error_description"] ) - @pytest.mark.anyio async def test_token_expired_auth_code( self, test_client, @@ -459,7 +455,6 @@ class TestAuthEndpoints: "authorization code has expired" in error_response["error_description"] ) - @pytest.mark.anyio @pytest.mark.parametrize( "registered_client", [ @@ -494,7 +489,6 @@ class TestAuthEndpoints: assert error_response["error"] == "invalid_request" assert "redirect_uri did not match" in error_response["error_description"] - @pytest.mark.anyio async def test_token_code_verifier_mismatch( self, test_client, registered_client, auth_code ): @@ -517,7 +511,6 @@ class TestAuthEndpoints: assert error_response["error"] == "invalid_grant" assert "incorrect code_verifier" in error_response["error_description"] - @pytest.mark.anyio async def test_token_invalid_refresh_token(self, test_client, registered_client): """Test token endpoint error - refresh token does not exist.""" # Try to use a non-existent refresh token @@ -535,7 +528,6 @@ class TestAuthEndpoints: assert error_response["error"] == "invalid_grant" assert "refresh token does not exist" in error_response["error_description"] - @pytest.mark.anyio async def test_token_expired_refresh_token( self, test_client, @@ -586,7 +578,6 @@ class TestAuthEndpoints: assert error_response["error"] == "invalid_grant" assert "refresh token has expired" in error_response["error_description"] - @pytest.mark.anyio async def test_token_invalid_scope( self, test_client, registered_client, auth_code, pkce_challenge ): @@ -624,7 +615,6 @@ class TestAuthEndpoints: assert error_response["error"] == "invalid_scope" assert "cannot request scope" in error_response["error_description"] - @pytest.mark.anyio async def test_client_registration( self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider ): @@ -652,7 +642,6 @@ class TestAuthEndpoints: # client_info["client_id"] # ) is not None - @pytest.mark.anyio async def test_client_registration_missing_required_fields( self, test_client: httpx.AsyncClient ): @@ -673,7 +662,6 @@ class TestAuthEndpoints: assert error_data["error"] == "invalid_client_metadata" assert error_data["error_description"] == "redirect_uris: Field required" - @pytest.mark.anyio async def test_client_registration_invalid_uri( self, test_client: httpx.AsyncClient ): @@ -696,7 +684,6 @@ class TestAuthEndpoints: "redirect_uris.0: Input should be a valid URL, relative URL without a base" ) - @pytest.mark.anyio async def test_client_registration_empty_redirect_uris( self, test_client: httpx.AsyncClient ): @@ -719,7 +706,6 @@ class TestAuthEndpoints: == "redirect_uris: List should have at least 1 item after validation, not 0" ) - @pytest.mark.anyio async def test_authorize_form_post( self, test_client: httpx.AsyncClient, @@ -763,7 +749,6 @@ class TestAuthEndpoints: assert "code" in query_params assert query_params["state"][0] == "test_form_state" - @pytest.mark.anyio async def test_authorization_get( self, test_client: httpx.AsyncClient, @@ -878,7 +863,6 @@ class TestAuthEndpoints: is None ) - @pytest.mark.anyio async def test_revoke_invalid_token(self, test_client, registered_client): """Test revoking an invalid token.""" response = await test_client.post( @@ -892,7 +876,6 @@ class TestAuthEndpoints: # per RFC, this should return 200 even if the token is invalid assert response.status_code == 200 - @pytest.mark.anyio async def test_revoke_with_malformed_token(self, test_client, registered_client): response = await test_client.post( "/revoke", @@ -908,7 +891,6 @@ class TestAuthEndpoints: assert error_response["error"] == "invalid_request" assert "token_type_hint" in error_response["error_description"] - @pytest.mark.anyio async def test_client_registration_disallowed_scopes( self, test_client: httpx.AsyncClient ): @@ -930,7 +912,6 @@ class TestAuthEndpoints: assert "scope" in error_data["error_description"] assert "admin" in error_data["error_description"] - @pytest.mark.anyio async def test_client_registration_default_scopes( self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider ): @@ -959,7 +940,6 @@ class TestAuthEndpoints: # Check that default scopes were applied assert registered_client.scope == "read write" - @pytest.mark.anyio async def test_client_registration_invalid_grant_type( self, test_client: httpx.AsyncClient ): @@ -986,7 +966,6 @@ class TestAuthEndpoints: class TestAuthorizeEndpointErrors: """Test error handling in the OAuth authorization endpoint.""" - @pytest.mark.anyio async def test_authorize_missing_client_id( self, test_client: httpx.AsyncClient, pkce_challenge ): @@ -1012,7 +991,6 @@ class TestAuthorizeEndpointErrors: # The response should include an error message about missing client_id assert "client_id" in response.text.lower() - @pytest.mark.anyio async def test_authorize_invalid_client_id( self, test_client: httpx.AsyncClient, pkce_challenge ): @@ -1038,7 +1016,6 @@ class TestAuthorizeEndpointErrors: # The response should include an error message about invalid client_id assert "client" in response.text.lower() - @pytest.mark.anyio async def test_authorize_missing_redirect_uri( self, test_client: httpx.AsyncClient, registered_client, pkce_challenge ): @@ -1064,7 +1041,6 @@ class TestAuthorizeEndpointErrors: redirect_url = response.headers["location"] assert redirect_url.startswith("https://client.example.com/callback") - @pytest.mark.anyio async def test_authorize_invalid_redirect_uri( self, test_client: httpx.AsyncClient, registered_client, pkce_challenge ): @@ -1092,7 +1068,6 @@ class TestAuthorizeEndpointErrors: # The response should include an error message about redirect_uri mismatch assert "redirect" in response.text.lower() - @pytest.mark.anyio @pytest.mark.parametrize( "registered_client", [ @@ -1130,7 +1105,6 @@ class TestAuthorizeEndpointErrors: # The response should include an error message about missing redirect_uri assert "redirect_uri" in response.text.lower() - @pytest.mark.anyio async def test_authorize_unsupported_response_type( self, test_client: httpx.AsyncClient, registered_client, pkce_challenge ): @@ -1164,7 +1138,6 @@ class TestAuthorizeEndpointErrors: assert "state" in query_params assert query_params["state"][0] == "test_state" - @pytest.mark.anyio async def test_authorize_missing_response_type( self, test_client: httpx.AsyncClient, registered_client, pkce_challenge ): @@ -1197,7 +1170,6 @@ class TestAuthorizeEndpointErrors: assert "state" in query_params assert query_params["state"][0] == "test_state" - @pytest.mark.anyio async def test_authorize_missing_pkce_challenge( self, test_client: httpx.AsyncClient, registered_client ): @@ -1228,7 +1200,6 @@ class TestAuthorizeEndpointErrors: assert "state" in query_params assert query_params["state"][0] == "test_state" - @pytest.mark.anyio async def test_authorize_invalid_scope( self, test_client: httpx.AsyncClient, registered_client, pkce_challenge ): diff --git a/tests/server/test_http_middleware.py b/tests/server/test_http_middleware.py index e48dc127b..7a1ab22f4 100644 --- a/tests/server/test_http_middleware.py +++ b/tests/server/test_http_middleware.py @@ -4,7 +4,6 @@ from collections.abc import Callable from typing import Any import httpx -import pytest from httpx import ASGITransport from starlette.middleware import Middleware from starlette.middleware.base import BaseHTTPMiddleware @@ -51,7 +50,6 @@ async def endpoint_handler(request: Request): return JSONResponse({"message": "Hello, world!"}) -@pytest.mark.asyncio async def test_sse_app_with_custom_middleware(): """Test that custom middleware works with SSE app.""" server = FastMCP(name="TestServer") @@ -82,7 +80,6 @@ async def test_sse_app_with_custom_middleware(): assert response.headers["X-Custom-Header"] == "test-value" -@pytest.mark.asyncio async def test_streamable_http_app_with_custom_middleware(): """Test that custom middleware works with StreamableHTTP app.""" server = FastMCP(name="TestServer") @@ -113,7 +110,6 @@ async def test_streamable_http_app_with_custom_middleware(): assert response.headers["X-Custom-Header"] == "test-value" -@pytest.mark.asyncio async def test_create_sse_app_with_custom_middleware(): """Test that custom middleware works with create_sse_app function.""" server = FastMCP(name="TestServer") @@ -149,7 +145,6 @@ async def test_create_sse_app_with_custom_middleware(): assert data["state"]["modified_by"] == "middleware" -@pytest.mark.asyncio async def test_create_streamable_http_app_with_custom_middleware(): """Test that custom middleware works with create_streamable_http_app function.""" server = FastMCP(name="TestServer") @@ -184,7 +179,6 @@ async def test_create_streamable_http_app_with_custom_middleware(): assert data["state"]["modified_by"] == "middleware" -@pytest.mark.asyncio async def test_multiple_middleware_ordering(): """Test that multiple middleware are applied in the correct order.""" server = FastMCP(name="TestServer") diff --git a/tests/server/test_lifespan.py b/tests/server/test_lifespan.py index 8b11ca825..ad041bbf9 100644 --- a/tests/server/test_lifespan.py +++ b/tests/server/test_lifespan.py @@ -1,10 +1,15 @@ """Tests for lifespan functionality in both low-level and FastMCP servers.""" +import os +import sys +import traceback from collections.abc import AsyncIterator from contextlib import asynccontextmanager +from pathlib import Path import anyio -import pytest +import httpx +import uvicorn from mcp.server.lowlevel.server import NotificationOptions, Server from mcp.server.models import InitializationOptions from mcp.shared.message import SessionMessage @@ -17,11 +22,13 @@ from mcp.types import ( JSONRPCRequest, ) from pydantic import TypeAdapter +from starlette.applications import Starlette +from starlette.routing import Mount from fastmcp import Context, FastMCP +from fastmcp.utilities.tests import run_server_in_process -@pytest.mark.anyio async def test_lowlevel_server_lifespan(): """Test that lifespan works in low-level server.""" @@ -132,7 +139,6 @@ async def test_lowlevel_server_lifespan(): tg.cancel_scope.cancel() -@pytest.mark.anyio async def test_fastmcp_server_lifespan(): """Test that lifespan works in FastMCP server.""" @@ -234,3 +240,157 @@ async def test_fastmcp_server_lifespan(): # Cancel server task tg.cancel_scope.cancel() + + +def run_server_with_incorrect_lifespan_setup( + host: str, port: int, server_log_file_path: str +) -> None: + os.makedirs(os.path.dirname(server_log_file_path), exist_ok=True) + + CUSTOM_LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "()": "uvicorn.logging.DefaultFormatter", + "fmt": "%(levelprefix)s %(asctime)s [%(name)s] %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + "use_colors": False, + }, + "access": { + "()": "uvicorn.logging.AccessFormatter", + "fmt": '%(levelprefix)s %(asctime)s [%(name)s] %(client_addr)s - "%(request_line)s" %(status_code)s', + "datefmt": "%Y-%m-%d %H:%M:%S", + "use_colors": False, + }, + }, + "handlers": { + "file_default": { + "formatter": "default", + "class": "logging.FileHandler", + "filename": server_log_file_path, + "mode": "w", + }, + "file_access": { + "formatter": "access", + "class": "logging.FileHandler", + "filename": server_log_file_path, + "mode": "a", + }, + }, + "loggers": { + "uvicorn": { # Catches uvicorn root logs + "handlers": ["file_default"], + "level": "DEBUG", + "propagate": False, + }, + "uvicorn.error": { + "handlers": ["file_default"], + "level": "DEBUG", + "propagate": False, + }, + "uvicorn.access": { + "handlers": ["file_access"], + "level": "INFO", + "propagate": False, + }, + }, + "root": { + "handlers": ["file_default"], + "level": "DEBUG", + }, + } + + try: + mcp = FastMCP() + + @mcp.tool("ping_tool", "A simple ping tool for the test server") + def ping_tool() -> str: + return "pong" + + mcp_asgi_app = mcp.http_app(transport="streamable-http") + + parent_app = Starlette( + routes=[Mount("/mounted_mcp", app=mcp_asgi_app)], + ) + + uvicorn.run( + parent_app, + host=host, + port=port, + log_config=CUSTOM_LOGGING_CONFIG, + log_level=None, + ) + sys.exit(0) + except Exception as e_outer: + with open(server_log_file_path, "a") as f_fallback: + f_fallback.write( + "--- FALLBACK EXCEPTION IN SERVER RUNNER (PRE-UVICORN) ---\n" + ) + f_fallback.write(f"{type(e_outer).__name__}: {e_outer}\n") + f_fallback.write(traceback.format_exc()) + sys.exit(1) + + +async def test_missing_lifespan_logs_informative_error(tmp_path: Path): + server_log_file = tmp_path / "server.log" + + with run_server_in_process( + run_server_with_incorrect_lifespan_setup, str(server_log_file) + ) as server_url: + full_mcp_path = server_url + "/mounted_mcp/mcp/" + + client_triggered_error = False + response_status = -1 + response_body = "" + try: + async with httpx.AsyncClient(timeout=10) as client: + response = await client.post( + full_mcp_path, + json={"id": 1, "method": "list_tools", "jsonrpc": "2.0"}, + ) + response_status = response.status_code + response_body = response.text + if response.status_code == 500: + client_triggered_error = True + else: + print( + f"Client received unexpected status code: {response.status_code} " + f"Response: {response_body[:500]}" + ) + except httpx.RequestError as e: + print(f"Client request failed with RequestError: {e}") + client_triggered_error = True + + assert client_triggered_error, ( + f"Client request did not result in a 500 error or a request error. " + f"Status: {response_status}, Body: {response_body[:500]}" + ) + + assert server_log_file.exists(), ( + f"Server log file was not created at {server_log_file}" + ) + log_content = server_log_file.read_text() + + print(f"--- Captured Server Log Content ({server_log_file}) ---") + print(log_content) + print("--- End Server Log Content ---") + + # Core assertions for the enhanced error message + assert ( + "FastMCP's StreamableHTTPSessionManager task group was not initialized" + in log_content + ) + assert "lifespan=mcp_app.lifespan" in log_content + assert "gofastmcp.com/deployment/asgi" in log_content + assert "Original error: Task group is not initialized" in log_content + + # Check for Uvicorn's own error logging wrapper for the request + assert "ERROR" in log_content # General check for ERROR level logs + assert "Exception in ASGI application" in log_content + + # Sanity checks for server operation and logging setup + assert "Uvicorn running on" in log_content + assert ( + "--- FALLBACK EXCEPTION IN SERVER RUNNER (PRE-UVICORN) ---" not in log_content + ) diff --git a/tests/server/test_openapi_path_parameters.py b/tests/server/test_openapi_path_parameters.py index e8b6121e5..9940594a8 100644 --- a/tests/server/test_openapi_path_parameters.py +++ b/tests/server/test_openapi_path_parameters.py @@ -87,7 +87,6 @@ async def test_fastmcp_from_openapi(array_path_spec, mock_client): assert "test-operation" in tool_names -@pytest.mark.asyncio async def test_array_path_parameter_handling(mock_client): """Test how array path parameters are handled.""" # Create a simple route with array path parameter @@ -158,7 +157,6 @@ async def test_array_path_parameter_handling(mock_client): ) -@pytest.mark.asyncio async def test_integration_array_path_parameter(array_path_spec, mock_client): """Integration test for array path parameters.""" # Create FastMCP from the spec @@ -192,7 +190,6 @@ async def test_integration_array_path_parameter(array_path_spec, mock_client): ) -@pytest.mark.asyncio async def test_complex_nested_array_path_parameter(mock_client): """Test handling of complex nested array path parameters.""" # Create a route with a path parameter that contains nested objects in an array @@ -262,7 +259,6 @@ async def test_complex_nested_array_path_parameter(mock_client): assert "{" not in called_url, "The URL should not contain Python object syntax" -@pytest.mark.asyncio async def test_array_query_param_with_fastapi(): """Test array query parameters using FastAPI and FastMCP.from_fastapi integration.""" # Create a FastAPI app with a route that has an array query parameter @@ -323,7 +319,6 @@ async def test_array_query_param_with_fastapi(): assert result_data == {"selected": ["monday", "tuesday"]} -@pytest.mark.asyncio async def test_array_query_parameter_format(mock_client): """Test that array query parameters are formatted as comma-separated values when explode=False.""" # Create a route with array query parameter @@ -394,7 +389,6 @@ async def test_array_query_parameter_format(mock_client): ) -@pytest.mark.asyncio async def test_array_query_parameter_exploded_format(mock_client): """Test that array query parameters are formatted as separate parameters when explode=True.""" # Create a route with array query parameter with explode=True (default) diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index ecb63bc8e..65f6fc71d 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -40,7 +40,6 @@ def test_streamable_http_app_deprecation_warning(): assert isinstance(app, Starlette) -@pytest.mark.asyncio async def test_run_sse_async_deprecation_warning(): """Test that run_sse_async raises a deprecation warning.""" server = FastMCP("TestServer") @@ -58,7 +57,6 @@ async def test_run_sse_async_deprecation_warning(): assert call_kwargs.get("transport") == "sse" -@pytest.mark.asyncio async def test_run_streamable_http_async_deprecation_warning(): """Test that run_streamable_http_async raises a deprecation warning.""" server = FastMCP("TestServer") diff --git a/tests/test_examples.py b/tests/test_examples.py index 1f68390d8..fcee6c521 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,6 +1,5 @@ """Tests for example servers""" -import pytest from mcp.types import ( PromptMessage, TextContent, @@ -11,7 +10,6 @@ from pydantic import AnyUrl from fastmcp import Client -@pytest.mark.anyio async def test_simple_echo(): """Test the simple echo server""" from examples.simple_echo import mcp @@ -23,7 +21,6 @@ async def test_simple_echo(): assert result[0].text == "hello" -@pytest.mark.anyio async def test_complex_inputs(): """Test the complex inputs server""" from examples.complex_inputs import mcp @@ -38,7 +35,6 @@ async def test_complex_inputs(): assert result[0].text == '[\n "bob",\n "alice",\n "charlie"\n]' -@pytest.mark.anyio async def test_desktop(monkeypatch): """Test the desktop server""" from examples.desktop import mcp @@ -58,7 +54,6 @@ async def test_desktop(monkeypatch): assert result[0].text == "Hello, rooter12!" -@pytest.mark.anyio async def test_echo(): """Test the echo server""" from examples.echo import mcp