Skip to content

Tool.from_function does not handle asynchronous callable objects #567

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

Open
stephanlensky opened this issue Apr 22, 2025 · 1 comment · May be fixed by #568
Open

Tool.from_function does not handle asynchronous callable objects #567

stephanlensky opened this issue Apr 22, 2025 · 1 comment · May be fixed by #568

Comments

@stephanlensky
Copy link

stephanlensky commented Apr 22, 2025

Hi there,

I am running into an issue when attempting to use callable objects with FastMCP.

Description / Reproducible Example

Regular, synchronous callable objects work (mostly) as expected, with the exception of having to set self.__name__:

import asyncio
from mcp.server.fastmcp import FastMCP

class TestTool:
    def __init__(self) -> None:
        self.__name__ = "TestTool"

    def __call__(self) -> str:
        return "Hello from TestTool!"

mcp = FastMCP()
mcp.add_tool(TestTool())

async def main() -> None:
    # prints "Hello from TestTool!"
    print(await mcp.call_tool("TestTool", {}))

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

However, making the __call__ method async breaks things:

import asyncio
from mcp.server.fastmcp import FastMCP

class TestTool:
    def __init__(self) -> None:
        self.__name__ = "TestTool"

    async def __call__(self) -> str:
        return "Hello from TestTool!"

mcp = FastMCP()
mcp.add_tool(TestTool())

async def main() -> None:
    # prints [TextContent(type='text', text='<coroutine object TestTool.__call__ at 0x103155300>', annotations=None)]
    print(await mcp.call_tool("TestTool", {}))

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

Simple Fix

I tracked this down to this line in Tool.from_function, which does not work properly for async callable objects:

is_async = inspect.iscoroutinefunction(fn)

I know this use-case is slightly out-of-the-ordinary, but it would be extremely helpful if we could improve this to work properly for callable objects as well.

There is some prior art here, Starlette checks if the provided callable is async like this, which works for both regular async functions and async callable objects:

def is_async_callable(obj: typing.Any) -> typing.Any:
    while isinstance(obj, functools.partial):
        obj = obj.func

    return inspect.iscoroutinefunction(obj) or (callable(obj) and inspect.iscoroutinefunction(obj.__call__))

Would you be open to accepting a pull request with a similar implementation?

Thank you!

@stephanlensky
Copy link
Author

stephanlensky commented Apr 22, 2025

For now, I was able to work around the issue by marking the class with @inspect.markcoroutinefunction:

import inspect

@inspect.markcoroutinefunction
class TestTool:
    def __init__(self) -> None:
        self.__name__ = "TestTool"

    async def __call__(self) -> str:
        return  "Hello from TestTool!"

@stephanlensky stephanlensky linked a pull request Apr 22, 2025 that will close this issue
9 tasks
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

Successfully merging a pull request may close this issue.

1 participant