From 25c6d20d7241e8a81fc342abd55f505c1c17259c Mon Sep 17 00:00:00 2001 From: shiraayal-tadata Date: Mon, 21 Apr 2025 18:58:53 +0300 Subject: [PATCH 1/4] fix issue 66 - ascii for json dumps to support chinese --- fastapi_mcp/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_mcp/server.py b/fastapi_mcp/server.py index 1a7b39e..dfe928c 100644 --- a/fastapi_mcp/server.py +++ b/fastapi_mcp/server.py @@ -259,7 +259,7 @@ async def _execute_api_tool( # 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) + result_text = json.dumps(result, indent=2, ensure_ascii=False) except json.JSONDecodeError: if hasattr(response, "text"): result_text = response.text From 21280e56814b8757c5fde192db9126985976c40d Mon Sep 17 00:00:00 2001 From: shiraayal-tadata Date: Mon, 21 Apr 2025 18:59:20 +0300 Subject: [PATCH 2/4] also in the mcp tools creation --- fastapi_mcp/openapi/convert.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fastapi_mcp/openapi/convert.py b/fastapi_mcp/openapi/convert.py index 439d9a0..8f16028 100644 --- a/fastapi_mcp/openapi/convert.py +++ b/fastapi_mcp/openapi/convert.py @@ -124,14 +124,14 @@ def convert_openapi_to_mcp_tools( # If we have an example response, add it to the docs if example_response: response_info += "\n\n**Example Response:**\n```json\n" - response_info += json.dumps(example_response, indent=2) + response_info += json.dumps(example_response, indent=2, ensure_ascii=False) response_info += "\n```" # Otherwise generate an example from the schema else: generated_example = generate_example_from_schema(display_schema) if generated_example: response_info += "\n\n**Example Response:**\n```json\n" - response_info += json.dumps(generated_example, indent=2) + response_info += json.dumps(generated_example, indent=2, ensure_ascii=False) response_info += "\n```" # Only include full schema information if requested @@ -141,15 +141,15 @@ def convert_openapi_to_mcp_tools( items_schema = display_schema["items"] 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 += json.dumps(items_schema, indent=2, ensure_ascii=False) response_info += "\n```" elif "properties" in display_schema: response_info += "\n\n**Output Schema:**\n```json\n" - response_info += json.dumps(display_schema, indent=2) + response_info += json.dumps(display_schema, indent=2, ensure_ascii=False) response_info += "\n```" else: response_info += "\n\n**Output Schema:**\n```json\n" - response_info += json.dumps(display_schema, indent=2) + response_info += json.dumps(display_schema, indent=2, ensure_ascii=False) response_info += "\n```" tool_description += response_info From 15cba7506c054d6d57e4e5e93f7daf71a77cf280 Mon Sep 17 00:00:00 2001 From: shiraayal-tadata Date: Tue, 22 Apr 2025 12:16:52 +0300 Subject: [PATCH 3/4] add tests --- tests/test_mcp_execute_api_tool.py | 192 +++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 tests/test_mcp_execute_api_tool.py diff --git a/tests/test_mcp_execute_api_tool.py b/tests/test_mcp_execute_api_tool.py new file mode 100644 index 0000000..cc05d34 --- /dev/null +++ b/tests/test_mcp_execute_api_tool.py @@ -0,0 +1,192 @@ +import pytest +from unittest.mock import AsyncMock, patch, MagicMock +from fastapi import FastAPI + +from fastapi_mcp import FastApiMCP +from mcp.types import TextContent + + +@pytest.mark.asyncio +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): + result = await mcp._execute_api_tool( + 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={} + ) + + +@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): + result = await mcp._execute_api_tool( + 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={} + ) + + +@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" + } + } + + # Execute the tool + 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 + ) + + # 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 + ) + + +@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 + } + + # 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 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): + result = await mcp._execute_api_tool( + 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={} + ) From 1dbd27551bca239bde52b1490961df89a48674ed Mon Sep 17 00:00:00 2001 From: shiraayal-tadata Date: Tue, 22 Apr 2025 12:19:56 +0300 Subject: [PATCH 4/4] fix --- fastapi_mcp/openapi/convert.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fastapi_mcp/openapi/convert.py b/fastapi_mcp/openapi/convert.py index 8f16028..439d9a0 100644 --- a/fastapi_mcp/openapi/convert.py +++ b/fastapi_mcp/openapi/convert.py @@ -124,14 +124,14 @@ def convert_openapi_to_mcp_tools( # If we have an example response, add it to the docs if example_response: response_info += "\n\n**Example Response:**\n```json\n" - response_info += json.dumps(example_response, indent=2, ensure_ascii=False) + response_info += json.dumps(example_response, indent=2) response_info += "\n```" # Otherwise generate an example from the schema else: generated_example = generate_example_from_schema(display_schema) if generated_example: response_info += "\n\n**Example Response:**\n```json\n" - response_info += json.dumps(generated_example, indent=2, ensure_ascii=False) + response_info += json.dumps(generated_example, indent=2) response_info += "\n```" # Only include full schema information if requested @@ -141,15 +141,15 @@ def convert_openapi_to_mcp_tools( items_schema = display_schema["items"] response_info += "\n\n**Output Schema:** Array of items with the following structure:\n```json\n" - response_info += json.dumps(items_schema, indent=2, ensure_ascii=False) + response_info += json.dumps(items_schema, indent=2) response_info += "\n```" elif "properties" in display_schema: response_info += "\n\n**Output Schema:**\n```json\n" - response_info += json.dumps(display_schema, indent=2, ensure_ascii=False) + response_info += json.dumps(display_schema, indent=2) response_info += "\n```" else: response_info += "\n\n**Output Schema:**\n```json\n" - response_info += json.dumps(display_schema, indent=2, ensure_ascii=False) + response_info += json.dumps(display_schema, indent=2) response_info += "\n```" tool_description += response_info