Skip to content

from_fastapi integration fails to handle dictionary/JSON return types from FastAPI endpoints #186

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

Closed
RoacherM opened this issue Apr 16, 2025 · 4 comments

Comments

@RoacherM
Copy link

When using FastMCP.from_fastapi() to integrate an existing FastAPI application, the MCP tools generated from FastAPI routes seem unable to correctly handle endpoints that return standard Python dictionaries (which FastAPI serializes to JSON).
While the underlying FastAPI endpoint executes successfully (confirmed via server logs and internal httpx calls returning 200 OK), the client.call_tool() fails on the client-side, often with errors related to task groups or response handling.
However, if the FastAPI endpoint is modified to return a simple string, or manually constructs and returns a List[TextContent] object, the client.call_tool() succeeds.
This suggests that the from_fastapi integration layer isn't properly converting the JSON response received from the FastAPI endpoint back into the List[Content] format required by the MCP protocol before sending it to the client. Ideally, from_fastapi should handle common FastAPI return types like dictionaries/JSON automatically, perhaps by converting them into a default TextContent representation or similar.

    from fastapi import FastAPI
    from fastmcp import FastMCP
    # from mcp.types import TextContent # Not needed for demonstrating the bug
    import logging

    logging.basicConfig(level=logging.INFO)
    log = logging.getLogger(__name__)

    fastapi_app = FastAPI(title="My Existing API")

    @fastapi_app.post("/items")
    def create_item(name: str, price: float):
        log.info(f"FastAPI endpoint called: name={name}, price={price}")
        # Returning a dictionary causes the client call to fail
        return {"id": 1, "name": name, "price": price}
        # Returning a string or List[TextContent] works:
        # return f"name: {name}, price: {price}" 
        # return [TextContent(text=f"name: {name}, price: {price}")]

    mcp_server = FastMCP.from_fastapi(fastapi_app)

    if __name__ == "__main__":
        log.info("Starting MCP server with default transport")
        # Using default transport (e.g., WebSocket), also observed with SSE
        mcp_server.run() 
@jlowin
Copy link
Owner

jlowin commented Apr 16, 2025

Thanks for the report -- I can replicate this issue on 2.1.2 but it appears to work well on main which should be released later today as 2.2.

Here's a one-file modification to run a client that I believe shows the difference (will fail on 2.1.2 and succeed on main):

# from mcp.types import TextContent # Not needed for demonstrating the bug
import asyncio
import logging

from fastapi import FastAPI

from fastmcp import Client, FastMCP

logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)

fastapi_app = FastAPI(title="My Existing API")


@fastapi_app.post("/items")
def create_item(name: str, price: float):
    log.info(f"FastAPI endpoint called: name={name}, price={price}")
    # Returning a dictionary causes the client call to fail
    return {"id": 1, "name": name, "price": price}
    # Returning a string or List[TextContent] works:
    # return f"name: {name}, price: {price}"
    # return [TextContent(text=f"name: {name}, price: {price}")]


mcp_server = FastMCP.from_fastapi(fastapi_app)


async def run_client():
    async with Client(mcp_server) as client:
        response = await client.call_tool(
            "create_item_items_post", {"name": "Test", "price": 100}
        )
        print(response)


if __name__ == "__main__":
    asyncio.run(run_client())

@jlowin
Copy link
Owner

jlowin commented Apr 16, 2025

Update: 2.2 is out now if you'd like to confirm

@RoacherM
Copy link
Author

Update: 2.2 is out now if you'd like to confirm

Thanks for your response. It did work right now with version 2.0.2.

@nightowl54321
Copy link

HI jlowin

Based on you comment.
Would you kindly share with me the api name mapping rule between server and client?
server side name "/items"
client call this api by "create_item_items_post"

Thanks
Cameron

#server fast api name
@fastapi_app.post("/items")

#client call this /items api
response = await client.call_tool(
"create_item_items_post", {"name": "Test", "price": 100}
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants