Advanced MCPs in Python: How Transitioning From FastMCP Wrappers Improved our FastAPI-MCP Tool

Miki Makhlevich
5 min readApr 10, 2025

--

After only a month of maintaining FastAPI-MCP, my team faced a critical realization that would force us to throw away weeks of work: the “easy path” we’d chosen was leading to a dead end. Our journey of transforming FastAPI endpoints into Model Context Protocol (MCP) tools taught us a lesson about when to abandon convenient abstractions and dive into the technical depths that most developers try to avoid.

If you find this interesting, please visit FastAPI-MCP and give it a star. We have exciting new features on the roadmap and are also open to contributions!

The Initial Approach: Using FastMCP

The journey began with clear intentions. When Shahar Abramov started building FastAPI-MCP alongside Shira Ayal, the path seemed clear: use FastMCP (the high-level wrapper) and parse the OpenAPI schema generated by FastAPI. This approach allowed fast implementation which enabled us to gain immediate attention from users, and appeared elegant on paper — FastAPI already provides detailed schema information about each endpoint, and FastMCP was extremely intuitive to use.

The implementation was straightforward. We’d:

  1. Extract the OpenAPI schema from the FastAPI application
  2. Parse the routes, parameters, and response types
  3. For each route, generate corresponding MCP tools via FastMCP
  4. Expose them through a server

This worked for basic use cases, but problems quickly emerged as users adopted the library for more complex scenarios.

Where Things Went Wrong

The first major issue appeared when handling input arguments. The functions were created statically rather than dynamically, which prevented proper parameter passing. This limitation became apparent when users attempted to use request bodies in their endpoints, resulting in server exceptions that made the tools effectively unusable for complex data scenarios. We didn’t notice this issue when publishing the library because our testing had focused solely on simple FastAPI examples, failing to account for more complex real-world implementations.

Our first attempt at a fix involved some creative metaprogramming, but the code quickly became convoluted and difficult to maintain. The approach required parsing the OpenAPI schema to determine the arguments each tool needed, then dynamically generating functions to serve as the foundation for these tools. The resulting code looked something like this:

params = _get_params_from_openapi_schema(openapi_schema)
params_str = ", ".join(params.values())
kwargs_str = ', '.join([f"'{k}': {k}" for k in params])

dynamic_function_body = f"""async def dynamic_function({params_str}):
kwargs = {{{kwargs_str}}}
return await function_template(**kwargs)
"""
exec(dynamic_function_body)

We immediately recognized this as unreadable, nearly impossible to debug, and simply a bad practice. Worse yet, each new edge case required additional patches, creating an increasingly fragile codebase.

Meanwhile, users began requesting crucial features that were technically possible but increasingly difficult to implement with our existing architecture:

  • Selectively exposing only certain endpoints — required convoluted filtering logic on top of our OpenAPI schema parser
  • Custom authentication flows — demanded more wrappers on top of FastMCP's structure
  • Better error handling — needed intricate error propagation through multiple layers of abstraction
  • Support for more complex FastAPI configurations — forced us to add special case handling for complex input arguments

With each new feature request, our codebase grew more entangled. Easy features required disproportionate effort, and the convoluted structure deterred new contributors. Even we struggled to navigate the maze of special cases we’d created.

The Refactor Decision

Almost immediately after merging this patch, we knew we had to find a better solution — one that would work with the architecture instead of fighting against it. We needed to reconsider our original decision, as the higher-level abstraction that initially seemed helpful was now constraining what the library could do.

While this meant “throwing away” significant work, the library was still relatively slim, and we recognized it would be better to make this change early rather than regret it later when the codebase grew larger and more complex.

Going Low-Level: Embracing the MCP SDK Core

The refactored approach abandoned FastMCP entirely in favor of working directly with the low-level MCP SDK. This meant more code to write by giving up on this easy-to-use library, but it provided significantly more control over how tools were created and registered.

We created a new FastApiMCP class for laying the ground for extension and composability, separating the MCP instance creation and mounting. The new code looks roughly like this (and I encourage you to look at the full code in the open-source library here):

from mcp.server.lowlevel.server import Server
from mcp.types import Tool

from fastapi_mcp.openapi.convert import convert_openapi_to_mcp_tools


Class FastApiMCP:
def __init__(
self,
# Input arguments
):
self.operation_map: Dict[str, Dict[str, Any]]
self.tools: List[Tool]
# ... Additional configurations ...
self.server = self.create_server()

def create_server(self) -> Server:
openapi_schema = get_openapi(...)
self.tools, self.operation_map = convert_openapi_to_mcp_tools()

# ... Additional logic, including defining handlers for call/list tools...
@mcp_server.list_tools()
async def handle_list_tools() -> List[Tool]:
return self.tools
# ...

mcp_server: Server = Server(
# Server parameters
)
return mcp_server

By working at this level, we gained several advantages:

  • Complete control over tool generation
  • Fine-grained endpoint selection
  • Flexible routing options for placing the MCP server on any FastAPI app or API router
  • Ability to deploy MCP server separately from the API service

But while gaining those important abilities, we lost the ability to create extra tools using the @mcp.tool() decorator, as it was inherent to FastMCP. We know we will need to address this in the future.

The Results: Stability and Flexibility

The refactored codebase is not only more stable but also more flexible. Users can now:

  • Precisely control which endpoints are exposed as MCP tools
  • Handle complex parameter types correctly
  • Easily contribute to the open-source with their desired additions

The code is also more maintainable because it follows FastAPI’s internal patterns rather than trying to reinterpret them through an additional layer of abstraction.

Lessons Learned

This experience reinforced some valuable lessons about library design:

  1. Higher-level abstractions come with trade-offs: While they can simplify common cases, they often make edge cases more difficult to handle.
  2. Understand the tools you’re wrapping: FastMCP was designed for a different use case than what we needed. It allowed us to move fast in this constantly changing world, but also limited us, and we should have recognized this sooner.
  3. Listen to user feedback: The requests for features were early signals that the architecture needed reconsideration.
  4. Don’t be afraid to refactor: Sometimes the best way forward is to step back and reconsider fundamental assumptions.

The Path Forward

With a more solid foundation in place, FastAPI-MCP is now better positioned to evolve with user needs. The refactored architecture opens possibilities for new features while maintaining the simple developer experience that was the original goal.

If you’re building tools in the MCP ecosystem, we’d love to hear about your experiences and what features would be most valuable to you. The project is open-source, and contributions are always welcome!

Written in collaboration with Shira Ayal and Shahar Abramov.

--

--

Miki Makhlevich
Miki Makhlevich

Written by Miki Makhlevich

Just a data geek trying to build something meaningful in this AI world

No responses yet