diff --git a/fastapi_mcp/openapi/convert.py b/fastapi_mcp/openapi/convert.py index 22e5c5e..9970a10 100644 --- a/fastapi_mcp/openapi/convert.py +++ b/fastapi_mcp/openapi/convert.py @@ -18,6 +18,7 @@ def convert_openapi_to_mcp_tools( openapi_schema: Dict[str, Any], describe_all_responses: bool = False, describe_full_response_schema: bool = False, + ignore_deprecated: bool = True, ) -> Tuple[List[types.Tool], Dict[str, Dict[str, Any]]]: """ Convert OpenAPI operations to MCP tools. @@ -26,6 +27,7 @@ def convert_openapi_to_mcp_tools( openapi_schema: The OpenAPI schema describe_all_responses: Whether to include all possible response schemas in tool descriptions describe_full_response_schema: Whether to include full response schema in tool descriptions + ignore_deprecated: Whether to ignore deprecated operations when converting to MCP tools Returns: A tuple containing: @@ -46,6 +48,11 @@ def convert_openapi_to_mcp_tools( logger.warning(f"Skipping non-HTTP method: {method}") continue + is_deprecated = operation.get("deprecated", False) + if is_deprecated and ignore_deprecated: + logger.warning(f"Skipping deprecated operation: {method} {path}") + continue + # Get operation metadata operation_id = operation.get("operationId") if not operation_id: diff --git a/fastapi_mcp/server.py b/fastapi_mcp/server.py index bb75106..ffe094b 100644 --- a/fastapi_mcp/server.py +++ b/fastapi_mcp/server.py @@ -84,6 +84,10 @@ def __init__( """ ), ] = ["authorization"], + ignore_deprecated: Annotated[ + bool, + Doc("Whether to ignore deprecated operations when converting OpenAPI to MCP tools. Defaults to True."), + ] = True, ): # Validate operation and tag filtering options if include_operations is not None and exclude_operations is not None: @@ -112,6 +116,8 @@ def __init__( if self._auth_config: self._auth_config = self._auth_config.model_validate(self._auth_config) + self._ignore_deprecated = ignore_deprecated + self._http_client = http_client or httpx.AsyncClient( transport=httpx.ASGITransport(app=self.fastapi, raise_app_exceptions=False), base_url=self._base_url, @@ -136,6 +142,7 @@ def setup_server(self) -> None: openapi_schema, describe_all_responses=self._describe_all_responses, describe_full_response_schema=self._describe_full_response_schema, + ignore_deprecated=self._ignore_deprecated, ) # Filter tools based on operation IDs and tags diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 79e403e..1d5d94b 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -581,3 +581,254 @@ async def empty_tags(): exclude_tags_mcp = FastApiMCP(app, exclude_tags=["items"]) assert len(exclude_tags_mcp.tools) == 1 assert {tool.name for tool in exclude_tags_mcp.tools} == {"empty_tags"} + + +def test_ignore_deprecated_default_behavior(simple_fastapi_app: FastAPI): + """Test that deprecated operations are ignored by default in FastApiMCP.""" + + # Add deprecated operations to the simple app + @simple_fastapi_app.get( + "/deprecated/items/", + response_model=list, + tags=["deprecated"], + operation_id="list_deprecated_items", + deprecated=True, + ) + async def list_deprecated_items(): + """[DEPRECATED] List all items (deprecated version).""" + return [] + + @simple_fastapi_app.get( + "/deprecated/items/{item_id}", + response_model=dict, + tags=["deprecated"], + operation_id="get_deprecated_item", + deprecated=True, + ) + async def get_deprecated_item(item_id: int): + """[DEPRECATED] Get a specific item by its ID (deprecated version).""" + return {"id": item_id} + + @simple_fastapi_app.post( + "/deprecated/items/", + response_model=dict, + tags=["deprecated"], + operation_id="create_deprecated_item", + deprecated=True, + ) + async def create_deprecated_item(): + """[DEPRECATED] Create a new item in the database (deprecated version).""" + return {"id": 1} + + mcp_server = FastApiMCP(simple_fastapi_app) + + # Should include regular operations but exclude deprecated ones + expected_operations = ["list_items", "get_item", "create_item", "update_item", "delete_item", "raise_error"] + deprecated_operations = ["list_deprecated_items", "get_deprecated_item", "create_deprecated_item"] + + assert len(mcp_server.tools) == len(expected_operations) + assert len(mcp_server.operation_map) == len(expected_operations) + + for op in expected_operations: + assert op in mcp_server.operation_map + + for op in deprecated_operations: + assert op not in mcp_server.operation_map + + for tool in mcp_server.tools: + assert tool.name in expected_operations + assert tool.name not in deprecated_operations + + +def test_ignore_deprecated_false(simple_fastapi_app: FastAPI): + """Test that deprecated operations are included when ignore_deprecated=False.""" + + # Add deprecated operations to the simple app + @simple_fastapi_app.get( + "/deprecated/items/", + response_model=list, + tags=["deprecated"], + operation_id="list_deprecated_items", + deprecated=True, + ) + async def list_deprecated_items(): + """[DEPRECATED] List all items (deprecated version).""" + return [] + + @simple_fastapi_app.get( + "/deprecated/items/{item_id}", + response_model=dict, + tags=["deprecated"], + operation_id="get_deprecated_item", + deprecated=True, + ) + async def get_deprecated_item(item_id: int): + """[DEPRECATED] Get a specific item by its ID (deprecated version).""" + return {"id": item_id} + + @simple_fastapi_app.post( + "/deprecated/items/", + response_model=dict, + tags=["deprecated"], + operation_id="create_deprecated_item", + deprecated=True, + ) + async def create_deprecated_item(): + """[DEPRECATED] Create a new item in the database (deprecated version).""" + return {"id": 1} + + mcp_server = FastApiMCP(simple_fastapi_app, ignore_deprecated=False) + + # Should include both regular and deprecated operations + expected_operations = ["list_items", "get_item", "create_item", "update_item", "delete_item", "raise_error"] + deprecated_operations = ["list_deprecated_items", "get_deprecated_item", "create_deprecated_item"] + all_operations = expected_operations + deprecated_operations + + assert len(mcp_server.tools) == len(all_operations) + assert len(mcp_server.operation_map) == len(all_operations) + + for op in all_operations: + assert op in mcp_server.operation_map + + for tool in mcp_server.tools: + assert tool.name in all_operations + + +def test_ignore_deprecated_true_explicit(simple_fastapi_app: FastAPI): + """Test that deprecated operations are excluded when ignore_deprecated=True explicitly.""" + + # Add deprecated operations to the simple app + @simple_fastapi_app.get( + "/deprecated/items/", + response_model=list, + tags=["deprecated"], + operation_id="list_deprecated_items", + deprecated=True, + ) + async def list_deprecated_items(): + """[DEPRECATED] List all items (deprecated version).""" + return [] + + @simple_fastapi_app.get( + "/deprecated/items/{item_id}", + response_model=dict, + tags=["deprecated"], + operation_id="get_deprecated_item", + deprecated=True, + ) + async def get_deprecated_item(item_id: int): + """[DEPRECATED] Get a specific item by its ID (deprecated version).""" + return {"id": item_id} + + @simple_fastapi_app.post( + "/deprecated/items/", + response_model=dict, + tags=["deprecated"], + operation_id="create_deprecated_item", + deprecated=True, + ) + async def create_deprecated_item(): + """[DEPRECATED] Create a new item in the database (deprecated version).""" + return {"id": 1} + + mcp_server = FastApiMCP(simple_fastapi_app, ignore_deprecated=True) + + # Should include regular operations but exclude deprecated ones + expected_operations = ["list_items", "get_item", "create_item", "update_item", "delete_item", "raise_error"] + deprecated_operations = ["list_deprecated_items", "get_deprecated_item", "create_deprecated_item"] + + assert len(mcp_server.tools) == len(expected_operations) + assert len(mcp_server.operation_map) == len(expected_operations) + + for op in expected_operations: + assert op in mcp_server.operation_map + + for op in deprecated_operations: + assert op not in mcp_server.operation_map + + for tool in mcp_server.tools: + assert tool.name in expected_operations + assert tool.name not in deprecated_operations + + +def test_ignore_deprecated_with_no_deprecated_operations(simple_fastapi_app: FastAPI): + """Test that ignore_deprecated works correctly when there are no deprecated operations.""" + mcp_server_ignore_true = FastApiMCP(simple_fastapi_app, ignore_deprecated=True) + mcp_server_ignore_false = FastApiMCP(simple_fastapi_app, ignore_deprecated=False) + + # Both should return the same results when there are no deprecated operations + assert len(mcp_server_ignore_true.tools) == len(mcp_server_ignore_false.tools) + assert len(mcp_server_ignore_true.operation_map) == len(mcp_server_ignore_false.operation_map) + + expected_operations = ["list_items", "get_item", "create_item", "update_item", "delete_item", "raise_error"] + + for op in expected_operations: + assert op in mcp_server_ignore_true.operation_map + assert op in mcp_server_ignore_false.operation_map + + for tool in mcp_server_ignore_true.tools: + assert tool.name in expected_operations + + for tool in mcp_server_ignore_false.tools: + assert tool.name in expected_operations + + +def test_ignore_deprecated_combined_with_filtering(simple_fastapi_app: FastAPI): + """Test that ignore_deprecated works correctly when combined with other filtering options.""" + + # Add deprecated operations to the simple app + @simple_fastapi_app.get( + "/deprecated/items/", + response_model=list, + tags=["deprecated"], + operation_id="list_deprecated_items", + deprecated=True, + ) + async def list_deprecated_items(): + """[DEPRECATED] List all items (deprecated version).""" + return [] + + @simple_fastapi_app.get( + "/deprecated/items/{item_id}", + response_model=dict, + tags=["deprecated"], + operation_id="get_deprecated_item", + deprecated=True, + ) + async def get_deprecated_item(item_id: int): + """[DEPRECATED] Get a specific item by its ID (deprecated version).""" + return {"id": item_id} + + @simple_fastapi_app.post( + "/deprecated/items/", + response_model=dict, + tags=["deprecated"], + operation_id="create_deprecated_item", + deprecated=True, + ) + async def create_deprecated_item(): + """[DEPRECATED] Create a new item in the database (deprecated version).""" + return {"id": 1} + + # Test with include_operations + mcp_server = FastApiMCP( + simple_fastapi_app, ignore_deprecated=True, include_operations=["list_items", "get_deprecated_item"] + ) + + # Should only include list_items since get_deprecated_item is deprecated and ignored + assert len(mcp_server.tools) == 1 + assert "list_items" in mcp_server.operation_map + assert "get_deprecated_item" not in mcp_server.operation_map + + # Test with include_tags + mcp_server = FastApiMCP(simple_fastapi_app, ignore_deprecated=True, include_tags=["deprecated"]) + + # Should not include any deprecated operations + assert len(mcp_server.tools) == 0 + + # Test with exclude_tags + mcp_server = FastApiMCP(simple_fastapi_app, ignore_deprecated=True, exclude_tags=["items"]) + + # Should only include raise_error since items are excluded and deprecated are ignored + assert len(mcp_server.tools) == 1 + assert "raise_error" in mcp_server.operation_map diff --git a/tests/test_openapi_conversion.py b/tests/test_openapi_conversion.py index aefe643..06fb4d5 100644 --- a/tests/test_openapi_conversion.py +++ b/tests/test_openapi_conversion.py @@ -422,3 +422,232 @@ def test_body_params_edge_cases(complex_fastapi_app: FastAPI): if "items" in properties: item_props = properties["items"]["items"]["properties"] assert "total" in item_props + + +def test_ignore_deprecated_default_behavior(simple_fastapi_app: FastAPI): + """Test that deprecated operations are ignored by default.""" + + # Add deprecated operations to the simple app + @simple_fastapi_app.get( + "/deprecated/items/", + response_model=list, + tags=["deprecated"], + operation_id="list_deprecated_items", + deprecated=True, + ) + async def list_deprecated_items(): + """[DEPRECATED] List all items (deprecated version).""" + return [] + + @simple_fastapi_app.get( + "/deprecated/items/{item_id}", + response_model=dict, + tags=["deprecated"], + operation_id="get_deprecated_item", + deprecated=True, + ) + async def get_deprecated_item(item_id: int): + """[DEPRECATED] Get a specific item by its ID (deprecated version).""" + return {"id": item_id} + + @simple_fastapi_app.post( + "/deprecated/items/", + response_model=dict, + tags=["deprecated"], + operation_id="create_deprecated_item", + deprecated=True, + ) + async def create_deprecated_item(): + """[DEPRECATED] Create a new item in the database (deprecated version).""" + return {"id": 1} + + openapi_schema = get_openapi( + title=simple_fastapi_app.title, + version=simple_fastapi_app.version, + openapi_version=simple_fastapi_app.openapi_version, + description=simple_fastapi_app.description, + routes=simple_fastapi_app.routes, + ) + + tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema) + + # Should include regular operations but exclude deprecated ones + expected_operations = ["list_items", "get_item", "create_item", "update_item", "delete_item", "raise_error"] + deprecated_operations = ["list_deprecated_items", "get_deprecated_item", "create_deprecated_item"] + + assert len(tools) == len(expected_operations) + assert len(operation_map) == len(expected_operations) + + for op in expected_operations: + assert op in operation_map + + for op in deprecated_operations: + assert op not in operation_map + + for tool in tools: + assert isinstance(tool, types.Tool) + assert tool.name in expected_operations + assert tool.name not in deprecated_operations + + +def test_ignore_deprecated_false(simple_fastapi_app: FastAPI): + """Test that deprecated operations are included when ignore_deprecated=False.""" + + # Add deprecated operations to the simple app + @simple_fastapi_app.get( + "/deprecated/items/", + response_model=list, + tags=["deprecated"], + operation_id="list_deprecated_items", + deprecated=True, + ) + async def list_deprecated_items(): + """[DEPRECATED] List all items (deprecated version).""" + return [] + + @simple_fastapi_app.get( + "/deprecated/items/{item_id}", + response_model=dict, + tags=["deprecated"], + operation_id="get_deprecated_item", + deprecated=True, + ) + async def get_deprecated_item(item_id: int): + """[DEPRECATED] Get a specific item by its ID (deprecated version).""" + return {"id": item_id} + + @simple_fastapi_app.post( + "/deprecated/items/", + response_model=dict, + tags=["deprecated"], + operation_id="create_deprecated_item", + deprecated=True, + ) + async def create_deprecated_item(): + """[DEPRECATED] Create a new item in the database (deprecated version).""" + return {"id": 1} + + openapi_schema = get_openapi( + title=simple_fastapi_app.title, + version=simple_fastapi_app.version, + openapi_version=simple_fastapi_app.openapi_version, + description=simple_fastapi_app.description, + routes=simple_fastapi_app.routes, + ) + + tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema, ignore_deprecated=False) + + # Should include both regular and deprecated operations + expected_operations = ["list_items", "get_item", "create_item", "update_item", "delete_item", "raise_error"] + deprecated_operations = ["list_deprecated_items", "get_deprecated_item", "create_deprecated_item"] + all_operations = expected_operations + deprecated_operations + + assert len(tools) == len(all_operations) + assert len(operation_map) == len(all_operations) + + for op in all_operations: + assert op in operation_map + + for tool in tools: + assert isinstance(tool, types.Tool) + assert tool.name in all_operations + + +def test_ignore_deprecated_true_explicit(simple_fastapi_app: FastAPI): + """Test that deprecated operations are excluded when ignore_deprecated=True explicitly.""" + + # Add deprecated operations to the simple app + @simple_fastapi_app.get( + "/deprecated/items/", + response_model=list, + tags=["deprecated"], + operation_id="list_deprecated_items", + deprecated=True, + ) + async def list_deprecated_items(): + """[DEPRECATED] List all items (deprecated version).""" + return [] + + @simple_fastapi_app.get( + "/deprecated/items/{item_id}", + response_model=dict, + tags=["deprecated"], + operation_id="get_deprecated_item", + deprecated=True, + ) + async def get_deprecated_item(item_id: int): + """[DEPRECATED] Get a specific item by its ID (deprecated version).""" + return {"id": item_id} + + @simple_fastapi_app.post( + "/deprecated/items/", + response_model=dict, + tags=["deprecated"], + operation_id="create_deprecated_item", + deprecated=True, + ) + async def create_deprecated_item(): + """[DEPRECATED] Create a new item in the database (deprecated version).""" + return {"id": 1} + + openapi_schema = get_openapi( + title=simple_fastapi_app.title, + version=simple_fastapi_app.version, + openapi_version=simple_fastapi_app.openapi_version, + description=simple_fastapi_app.description, + routes=simple_fastapi_app.routes, + ) + + tools, operation_map = convert_openapi_to_mcp_tools(openapi_schema, ignore_deprecated=True) + + # Should include regular operations but exclude deprecated ones + expected_operations = ["list_items", "get_item", "create_item", "update_item", "delete_item", "raise_error"] + deprecated_operations = ["list_deprecated_items", "get_deprecated_item", "create_deprecated_item"] + + assert len(tools) == len(expected_operations) + assert len(operation_map) == len(expected_operations) + + for op in expected_operations: + assert op in operation_map + + for op in deprecated_operations: + assert op not in operation_map + + for tool in tools: + assert isinstance(tool, types.Tool) + assert tool.name in expected_operations + assert tool.name not in deprecated_operations + + +def test_ignore_deprecated_with_no_deprecated_operations(simple_fastapi_app: FastAPI): + """Test that ignore_deprecated works correctly when there are no deprecated operations.""" + openapi_schema = get_openapi( + title=simple_fastapi_app.title, + version=simple_fastapi_app.version, + openapi_version=simple_fastapi_app.openapi_version, + description=simple_fastapi_app.description, + routes=simple_fastapi_app.routes, + ) + + tools_ignore_true, operation_map_ignore_true = convert_openapi_to_mcp_tools(openapi_schema, ignore_deprecated=True) + tools_ignore_false, operation_map_ignore_false = convert_openapi_to_mcp_tools( + openapi_schema, ignore_deprecated=False + ) + + # Both should return the same results when there are no deprecated operations + assert len(tools_ignore_true) == len(tools_ignore_false) + assert len(operation_map_ignore_true) == len(operation_map_ignore_false) + + expected_operations = ["list_items", "get_item", "create_item", "update_item", "delete_item", "raise_error"] + + for op in expected_operations: + assert op in operation_map_ignore_true + assert op in operation_map_ignore_false + + for tool in tools_ignore_true: + assert isinstance(tool, types.Tool) + assert tool.name in expected_operations + + for tool in tools_ignore_false: + assert isinstance(tool, types.Tool) + assert tool.name in expected_operations