Repository: jlowin/fastmcp
Pull request #447 (2025-05-14 17:42:57)

Add reprs for OpenAPI objects
OpenAPI object reprs hit recursion errors, so we create them by hand.


Patch:
diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py
index fd46ac3..19ee1e4 100644
--- a/src/fastmcp/server/openapi.py
+++ b/src/fastmcp/server/openapi.py
@@ -138,6 +138,10 @@ class OpenAPITool(Tool):
         self._route = route
         self._timeout = timeout
 
+    def __repr__(self) -> str:
+        """Custom representation to prevent recursion errors when printing."""
+        return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
+
     async def _execute_request(self, *args, **kwargs):
         """Execute the HTTP request based on the route configuration."""
         context = kwargs.get("context")
@@ -287,6 +291,10 @@ class OpenAPIResource(Resource):
         self._route = route
         self._timeout = timeout
 
+    def __repr__(self) -> str:
+        """Custom representation to prevent recursion errors when printing."""
+        return f"OpenAPIResource(name={self.name!r}, uri={self.uri!r}, path={self._route.path})"
+
     async def read(self) -> str | bytes:
         """Fetch the resource data by making an HTTP request."""
         try:
@@ -397,6 +405,10 @@ class OpenAPIResourceTemplate(ResourceTemplate):
         self._route = route
         self._timeout = timeout
 
+    def __repr__(self) -> str:
+        """Custom representation to prevent recursion errors when printing."""
+        return f"OpenAPIResourceTemplate(name={self.name!r}, uri_template={self.uri_template!r}, path={self._route.path})"
+
     async def create_resource(
         self,
         uri: str,


Test patch:
diff --git a/tests/server/test_openapi.py b/tests/server/test_openapi.py
index b7a6df1..ee2c0f7 100644
--- a/tests/server/test_openapi.py
+++ b/tests/server/test_openapi.py
@@ -1771,3 +1771,49 @@ class TestFastAPIDescriptionPropagation:
                 "name parameter missing from Tool schema in client API"
             )
             # We don't test for the description field content as it may not be consistently propagated
+
+
+class TestReprMethods:
+    """Tests for the custom __repr__ methods of OpenAPI objects."""
+
+    async def test_openapi_tool_repr(self, fastmcp_openapi_server: FastMCPOpenAPI):
+        """Test that OpenAPITool's __repr__ method works without recursion errors."""
+        tools = fastmcp_openapi_server._tool_manager.list_tools()
+        tool = next(iter(tools))
+
+        # Verify repr doesn't cause recursion and contains expected elements
+        tool_repr = repr(tool)
+        assert "OpenAPITool" in tool_repr
+        assert f"name={tool.name!r}" in tool_repr
+        assert "method=" in tool_repr
+        assert "path=" in tool_repr
+
+    async def test_openapi_resource_repr(self, fastmcp_openapi_server: FastMCPOpenAPI):
+        """Test that OpenAPIResource's __repr__ method works without recursion errors."""
+        resources = list(
+            fastmcp_openapi_server._resource_manager.get_resources().values()
+        )
+        resource = next(iter(resources))
+
+        # Verify repr doesn't cause recursion and contains expected elements
+        resource_repr = repr(resource)
+        assert "OpenAPIResource" in resource_repr
+        assert f"name={resource.name!r}" in resource_repr
+        assert "uri=" in resource_repr
+        assert "path=" in resource_repr
+
+    async def test_openapi_resource_template_repr(
+        self, fastmcp_openapi_server: FastMCPOpenAPI
+    ):
+        """Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors."""
+        templates = list(
+            fastmcp_openapi_server._resource_manager.get_templates().values()
+        )
+        template = next(iter(templates))
+
+        # Verify repr doesn't cause recursion and contains expected elements
+        template_repr = repr(template)
+        assert "OpenAPIResourceTemplate" in template_repr
+        assert f"name={template.name!r}" in template_repr
+        assert "uri_template=" in template_repr
+        assert "path=" in template_repr
diff --git a/tests/utilities/openapi/test_openapi_fastapi.py b/tests/utilities/openapi/test_openapi_fastapi.py
index 94d0afe..3f0ee8b 100644
--- a/tests/utilities/openapi/test_openapi_fastapi.py
+++ b/tests/utilities/openapi/test_openapi_fastapi.py
@@ -520,3 +520,20 @@ def test_duplicate_tags_handling(fastapi_server):
     # We'll test both possibilities to be safe
     assert "duplicate" in test_route.tags, "Tag 'duplicate' should be present"
     assert "items" in test_route.tags, "Tag 'items' should be present"
+
+
+def test_repr_http_routes(parsed_routes):
+    """Test that HTTPRoute objects can be represented without recursion errors."""
+    # Test repr on all parsed routes
+    for route in parsed_routes:
+        route_repr = repr(route)
+
+        # Verify repr contains essential information
+        assert route.method in route_repr, f"Method {route.method} missing from repr"
+        assert route.path in route_repr, f"Path {route.path} missing from repr"
+
+        # If operation_id exists, it should be in the repr
+        if route.operation_id:
+            assert route.operation_id in route_repr, (
+                f"Operation ID {route.operation_id} missing from repr"
+            )