Skip to content

Bugfix/issue 66 #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fastapi_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
192 changes: 192 additions & 0 deletions tests/test_mcp_execute_api_tool.py
Original file line number Diff line number Diff line change
@@ -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={}
)