diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 848b3cb..fdbeef7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ First off, thank you for considering contributing to FastAPI-MCP! ```bash git clone https://github.com/YOUR-USERNAME/fastapi_mcp.git - cd fastapi-mcp + cd fastapi_mcp # Add the upstream remote git remote add upstream https://github.com/tadata-org/fastapi_mcp.git diff --git a/examples/01_basic_usage_example.py b/examples/01_basic_usage_example.py index 470dab9..d387458 100644 --- a/examples/01_basic_usage_example.py +++ b/examples/01_basic_usage_example.py @@ -1,4 +1,4 @@ -from examples.shared.apps.items import app # The FastAPI app +from examples.shared.apps.items import app # The FastAPI app from examples.shared.setup import setup_logging from fastapi_mcp import FastApiMCP @@ -15,4 +15,4 @@ if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/examples/02_full_schema_description_example.py b/examples/02_full_schema_description_example.py index 9750c33..9210e18 100644 --- a/examples/02_full_schema_description_example.py +++ b/examples/02_full_schema_description_example.py @@ -1,8 +1,8 @@ - """ This example shows how to describe the full response schema instead of just a response example. """ -from examples.shared.apps.items import app # The FastAPI app + +from examples.shared.apps.items import app # The FastAPI app from examples.shared.setup import setup_logging from fastapi_mcp import FastApiMCP @@ -22,5 +22,5 @@ if __name__ == "__main__": import uvicorn - + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/examples/03_custom_exposed_endpoints_example.py b/examples/03_custom_exposed_endpoints_example.py index 59e46e6..8d21ed8 100644 --- a/examples/03_custom_exposed_endpoints_example.py +++ b/examples/03_custom_exposed_endpoints_example.py @@ -6,7 +6,8 @@ - You can combine operation filtering with tag filtering (e.g., use `include_operations` with `include_tags`) - When combining filters, a greedy approach will be taken. Endpoints matching either criteria will be included """ -from examples.shared.apps.items import app # The FastAPI app + +from examples.shared.apps.items import app # The FastAPI app from examples.shared.setup import setup_logging from fastapi_mcp import FastApiMCP @@ -24,7 +25,7 @@ # Filter by excluding specific operation IDs exclude_operations_mcp = FastApiMCP( - app, + app, name="Item API MCP - Excluded Operations", exclude_operations=["create_item", "update_item", "delete_item"], ) diff --git a/examples/04_separate_server_example.py b/examples/04_separate_server_example.py index e468557..80f10da 100644 --- a/examples/04_separate_server_example.py +++ b/examples/04_separate_server_example.py @@ -2,6 +2,7 @@ This example shows how to run the MCP server and the FastAPI app separately. You can create an MCP server from one FastAPI app, and mount it to a different app. """ + from fastapi import FastAPI from examples.shared.apps.items import app @@ -30,4 +31,4 @@ if __name__ == "__main__": import uvicorn - uvicorn.run(mcp_app, host="0.0.0.0", port=8000) \ No newline at end of file + uvicorn.run(mcp_app, host="0.0.0.0", port=8000) diff --git a/examples/05_reregister_tools_example.py b/examples/05_reregister_tools_example.py index d30ce49..14e6f41 100644 --- a/examples/05_reregister_tools_example.py +++ b/examples/05_reregister_tools_example.py @@ -1,15 +1,16 @@ """ This example shows how to re-register tools if you add endpoints after the MCP server was created. """ -from examples.shared.apps.items import app # The FastAPI app + +from examples.shared.apps.items import app # The FastAPI app from examples.shared.setup import setup_logging from fastapi_mcp import FastApiMCP setup_logging() -mcp = FastApiMCP(app) # Add MCP server to the FastAPI app -mcp.mount() # MCP server +mcp = FastApiMCP(app) # Add MCP server to the FastAPI app +mcp.mount() # MCP server # This endpoint will not be registered as a tool, since it was added after the MCP instance was created @@ -24,5 +25,5 @@ async def new_endpoint(): if __name__ == "__main__": import uvicorn - + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/examples/06_custom_mcp_router_example.py b/examples/06_custom_mcp_router_example.py index 83ea6ad..b69ac09 100644 --- a/examples/06_custom_mcp_router_example.py +++ b/examples/06_custom_mcp_router_example.py @@ -1,7 +1,8 @@ """ This example shows how to mount the MCP server to a specific APIRouter, giving a custom mount path. """ -from examples.shared.apps.items import app # The FastAPI app + +from examples.shared.apps.items import app # The FastAPI app from examples.shared.setup import setup_logging from fastapi import APIRouter @@ -9,7 +10,7 @@ setup_logging() -other_router = APIRouter(prefix="/other/route") +other_router = APIRouter(prefix="/other/route") app.include_router(other_router) mcp = FastApiMCP(app) @@ -21,5 +22,5 @@ if __name__ == "__main__": import uvicorn - + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/examples/07_configure_http_timeout_example.py b/examples/07_configure_http_timeout_example.py index eaab570..036c103 100644 --- a/examples/07_configure_http_timeout_example.py +++ b/examples/07_configure_http_timeout_example.py @@ -2,7 +2,8 @@ This example shows how to configure the HTTP client timeout for the MCP server. In case you have API endpoints that take longer than 5 seconds to respond, you can increase the timeout. """ -from examples.shared.apps.items import app # The FastAPI app + +from examples.shared.apps.items import app # The FastAPI app from examples.shared.setup import setup_logging import httpx @@ -12,14 +13,11 @@ setup_logging() -mcp = FastApiMCP( - app, - http_client=httpx.AsyncClient(timeout=20) -) +mcp = FastApiMCP(app, http_client=httpx.AsyncClient(timeout=20)) mcp.mount() if __name__ == "__main__": import uvicorn - + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/examples/08_auth_example_token_passthrough.py b/examples/08_auth_example_token_passthrough.py index 8f0b8f4..422f4ab 100644 --- a/examples/08_auth_example_token_passthrough.py +++ b/examples/08_auth_example_token_passthrough.py @@ -21,7 +21,8 @@ } ``` """ -from examples.shared.apps.items import app # The FastAPI app + +from examples.shared.apps.items import app # The FastAPI app from examples.shared.setup import setup_logging from fastapi import Depends @@ -34,11 +35,13 @@ # Scheme for the Authorization header token_auth_scheme = HTTPBearer() + # Create a private endpoint @app.get("/private") -async def private(token = Depends(token_auth_scheme)): +async def private(token=Depends(token_auth_scheme)): return token.credentials + # Create the MCP server with the token auth scheme mcp = FastApiMCP( app, @@ -54,5 +57,5 @@ async def private(token = Depends(token_auth_scheme)): if __name__ == "__main__": import uvicorn - - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/fastapi_mcp/openapi/convert.py b/fastapi_mcp/openapi/convert.py index 22e5c5e..7bcec3c 100644 --- a/fastapi_mcp/openapi/convert.py +++ b/fastapi_mcp/openapi/convert.py @@ -140,7 +140,10 @@ def convert_openapi_to_mcp_tools( if display_schema.get("type") == "array" and "items" in display_schema: items_schema = display_schema["items"] - response_info += "\n\n**Output Schema:** Array of items with the following structure:\n```json\n" + response_info += ( + "\n\n**Output Schema:** Array of items with the following " + "structure:\n```json\n" + ) response_info += json.dumps(items_schema, indent=2) response_info += "\n```" elif "properties" in display_schema: diff --git a/fastapi_mcp/openapi/utils.py b/fastapi_mcp/openapi/utils.py index 1821d57..a434eaf 100644 --- a/fastapi_mcp/openapi/utils.py +++ b/fastapi_mcp/openapi/utils.py @@ -5,6 +5,11 @@ def get_single_param_type_from_schema(param_schema: Dict[str, Any]) -> str: """ Get the type of a parameter from the schema. If the schema is a union type, return the first type. + + Args: + param_schema (Dict[str, Any]): Schema definition. + Returns: + str: The type of a parameter. """ if "anyOf" in param_schema: types = {schema.get("type") for schema in param_schema["anyOf"] if schema.get("type")} @@ -25,7 +30,7 @@ def resolve_schema_references(schema_part: Dict[str, Any], reference_schema: Dic reference_schema: The complete schema used to resolve references from Returns: - The schema with references resolved + dict: The schema with references resolved """ # Make a copy to avoid modifying the input schema schema_part = schema_part.copy() @@ -65,7 +70,7 @@ def clean_schema_for_display(schema: Dict[str, Any]) -> Dict[str, Any]: schema: The schema to clean Returns: - The cleaned schema + dict: The cleaned schema """ # Make a copy to avoid modifying the input schema schema = schema.copy() diff --git a/fastapi_mcp/server.py b/fastapi_mcp/server.py index 38c99d9..8c3ac38 100644 --- a/fastapi_mcp/server.py +++ b/fastapi_mcp/server.py @@ -429,7 +429,8 @@ async def _execute_api_tool( logger.debug(f"Making {method.upper()} request to {path}") response = await self._request(client, method, path, query, headers, body) - # TODO: Better typing for the AsyncClientProtocol. It should return a ResponseProtocol that has a json() method that returns a dict/list/etc. + # TODO: Better typing for the AsyncClientProtocol. It should return a ResponseProtocol that has a json() + # method that returns a dict/list/etc. try: result = response.json() result_text = json.dumps(result, indent=2, ensure_ascii=False) @@ -439,8 +440,10 @@ async def _execute_api_tool( else: result_text = response.content - # If not raising an exception, the MCP server will return the result as a regular text response, without marking it as an error. - # TODO: Use a raise_for_status() method on the response (it needs to also be implemented in the AsyncClientProtocol) + # If not raising an exception, the MCP server will return the result as a regular text response, + # without marking it as an error. + # TODO: Use a raise_for_status() method on the response (it needs to also + # be implemented in the AsyncClientProtocol) if 400 <= response.status_code < 600: raise Exception( f"Error calling {tool_name}. Status code: {response.status_code}. Response: {response.text}" @@ -486,7 +489,7 @@ def _filter_tools(self, tools: List[types.Tool], openapi_schema: Dict[str, Any]) openapi_schema: The OpenAPI schema Returns: - Filtered list of tools + list: Filtered list of tools """ if ( self._include_operations is None diff --git a/tests/test_mcp_execute_api_tool.py b/tests/test_mcp_execute_api_tool.py index cc05d34..a492f65 100644 --- a/tests/test_mcp_execute_api_tool.py +++ b/tests/test_mcp_execute_api_tool.py @@ -10,183 +10,150 @@ async def test_execute_api_tool_success(simple_fastapi_app: FastAPI): """Test successful execution of an API tool.""" mcp = FastApiMCP(simple_fastapi_app) - + # Mock the HTTP client response mock_response = MagicMock() mock_response.json.return_value = {"id": 1, "name": "Test Item"} mock_response.status_code = 200 mock_response.text = '{"id": 1, "name": "Test Item"}' - + # Mock the HTTP client mock_client = AsyncMock() mock_client.get.return_value = mock_response - + # Test parameters tool_name = "get_item" arguments = {"item_id": 1} - + # Execute the tool - with patch.object(mcp, '_http_client', mock_client): + with patch.object(mcp, "_http_client", mock_client): result = await mcp._execute_api_tool( - client=mock_client, - tool_name=tool_name, - arguments=arguments, - operation_map=mcp.operation_map + client=mock_client, tool_name=tool_name, arguments=arguments, operation_map=mcp.operation_map ) - + # Verify the result assert len(result) == 1 assert isinstance(result[0], TextContent) assert result[0].text == '{\n "id": 1,\n "name": "Test Item"\n}' - + # Verify the HTTP client was called correctly - mock_client.get.assert_called_once_with( - "/items/1", - params={}, - headers={} - ) + mock_client.get.assert_called_once_with("/items/1", params={}, headers={}) @pytest.mark.asyncio async def test_execute_api_tool_with_query_params(simple_fastapi_app: FastAPI): """Test execution of an API tool with query parameters.""" mcp = FastApiMCP(simple_fastapi_app) - + # Mock the HTTP client response mock_response = MagicMock() mock_response.json.return_value = [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}] mock_response.status_code = 200 mock_response.text = '[{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]' - + # Mock the HTTP client mock_client = AsyncMock() mock_client.get.return_value = mock_response - + # Test parameters tool_name = "list_items" arguments = {"skip": 0, "limit": 2} - + # Execute the tool - with patch.object(mcp, '_http_client', mock_client): + with patch.object(mcp, "_http_client", mock_client): result = await mcp._execute_api_tool( - client=mock_client, - tool_name=tool_name, - arguments=arguments, - operation_map=mcp.operation_map + client=mock_client, tool_name=tool_name, arguments=arguments, operation_map=mcp.operation_map ) - + # Verify the result assert len(result) == 1 assert isinstance(result[0], TextContent) - + # Verify the HTTP client was called with query parameters - mock_client.get.assert_called_once_with( - "/items/", - params={"skip": 0, "limit": 2}, - headers={} - ) + mock_client.get.assert_called_once_with("/items/", params={"skip": 0, "limit": 2}, headers={}) @pytest.mark.asyncio async def test_execute_api_tool_with_body(simple_fastapi_app: FastAPI): """Test execution of an API tool with request body.""" mcp = FastApiMCP(simple_fastapi_app) - + # Mock the HTTP client response mock_response = MagicMock() mock_response.json.return_value = {"id": 1, "name": "New Item"} mock_response.status_code = 200 mock_response.text = '{"id": 1, "name": "New Item"}' - + # Mock the HTTP client mock_client = AsyncMock() mock_client.post.return_value = mock_response - + # Test parameters tool_name = "create_item" arguments = { - "item": { - "id": 1, - "name": "New Item", - "price": 10.0, - "tags": ["tag1"], - "description": "New item description" - } + "item": {"id": 1, "name": "New Item", "price": 10.0, "tags": ["tag1"], "description": "New item description"} } - + # Execute the tool - with patch.object(mcp, '_http_client', mock_client): + with patch.object(mcp, "_http_client", mock_client): result = await mcp._execute_api_tool( - client=mock_client, - tool_name=tool_name, - arguments=arguments, - operation_map=mcp.operation_map + client=mock_client, tool_name=tool_name, arguments=arguments, operation_map=mcp.operation_map ) - + # Verify the result assert len(result) == 1 assert isinstance(result[0], TextContent) - + # Verify the HTTP client was called with the request body - mock_client.post.assert_called_once_with( - "/items/", - params={}, - headers={}, - json=arguments - ) + mock_client.post.assert_called_once_with("/items/", params={}, headers={}, json=arguments) @pytest.mark.asyncio async def test_execute_api_tool_with_non_ascii_chars(simple_fastapi_app: FastAPI): """Test execution of an API tool with non-ASCII characters.""" mcp = FastApiMCP(simple_fastapi_app) - + # Test data with both ASCII and non-ASCII characters test_data = { "id": 1, "name": "你好 World", # Chinese characters + ASCII "price": 10.0, "tags": ["tag1", "标签2"], # Chinese characters in tags - "description": "这是一个测试描述" # All Chinese characters + "description": "这是一个测试描述", # All Chinese characters } - + # Mock the HTTP client response mock_response = MagicMock() mock_response.json.return_value = test_data mock_response.status_code = 200 - mock_response.text = '{"id": 1, "name": "你好 World", "price": 10.0, "tags": ["tag1", "标签2"], "description": "这是一个测试描述"}' - + mock_response.text = ( + '{"id": 1, "name": "你好 World", "price": 10.0, "tags": ["tag1", "标签2"], "description": "这是一个测试描述"}' + ) + # Mock the HTTP client mock_client = AsyncMock() mock_client.get.return_value = mock_response - + # Test parameters tool_name = "get_item" arguments = {"item_id": 1} - + # Execute the tool - with patch.object(mcp, '_http_client', mock_client): + with patch.object(mcp, "_http_client", mock_client): result = await mcp._execute_api_tool( - client=mock_client, - tool_name=tool_name, - arguments=arguments, - operation_map=mcp.operation_map + client=mock_client, tool_name=tool_name, arguments=arguments, operation_map=mcp.operation_map ) - + # Verify the result assert len(result) == 1 assert isinstance(result[0], TextContent) - + # Verify that the response contains both ASCII and non-ASCII characters response_text = result[0].text assert "你好" in response_text # Chinese characters preserved assert "World" in response_text # ASCII characters preserved assert "标签2" in response_text # Chinese characters in tags preserved assert "这是一个测试描述" in response_text # All Chinese description preserved - + # Verify the HTTP client was called correctly - mock_client.get.assert_called_once_with( - "/items/1", - params={}, - headers={} - ) + mock_client.get.assert_called_once_with("/items/1", params={}, headers={})