PrefectHQ/fastmcp · error · TypeError

First argument to @tool must be a function, string, or None,

Error message

First argument to @tool must be a function, string, or None, got {type(name_or_fn)}

What it means

The first positional argument to @tool must be a function, a string (tool name), or None. Anything else — a class, dict, partial, bound method wrapper of unsupported kind, etc. — is rejected with a TypeError showing the received type.

Source

Thrown at fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py:338

            self.add_tool(fn)
            return fn

        if inspect.isroutine(name_or_fn):
            return decorate_and_register(name_or_fn, name)

        elif isinstance(name_or_fn, str):
            # Case 3: @tool("custom_name") - name passed as first argument
            if name is not None:
                raise TypeError(
                    "Cannot specify both a name as first argument and as keyword argument. "
                    f"Use either @tool('{name_or_fn}') or @tool(name='{name}'), not both."
                )
            tool_name = name_or_fn
        elif name_or_fn is None:
            # Case 4: @tool() or @tool(name="something") - use keyword name
            tool_name = name
        else:
            raise TypeError(
                f"First argument to @tool must be a function, string, or None, got {type(name_or_fn)}"
            )

        # Return partial for cases where we need to wait for the function
        return partial(
            self.tool,
            name=tool_name,
            version=version,
            title=title,
            description=description,
            icons=icons,
            tags=tags,
            output_schema=output_schema,
            annotations=annotations,
            meta=meta,
            enabled=enabled,
            task=task,
            timeout=timeout,

View on GitHub (pinned to 1f02114297)

Solutions

  1. Pass the function itself, a string name, or nothing: @tool, @tool('name'), or @tool().
  2. If you called the function by mistake, drop the parentheses so the callable itself is passed.
  3. Use keyword arguments (name=, description=) for everything besides the function/name.

Example fix

// before
@tool(compute(x=1))
def handler(x: int) -> int:
    ...

// after
@tool('compute')
def handler(x: int) -> int:
    ...
Defensive patterns

Strategy: type-guard

Validate before calling

def check_first_arg(name_or_fn):
    if not (inspect.isroutine(name_or_fn) or isinstance(name_or_fn, str) or name_or_fn is None):
        raise TypeError(f'@tool first arg must be function/str/None, got {type(name_or_fn)}')

check_first_arg(compute)  # ok
check_first_arg('name')   # ok

Type guard

def is_valid_tool_first_arg(x) -> bool:
    import inspect
    return inspect.isroutine(x) or isinstance(x, str) or x is None

Try / catch

try:
    mcp.add_tool(make_tool(x))
except TypeError as e:
    if 'First argument to @tool' in str(e):
        logging.error('pass the function itself, not its result')
    raise

Prevention

When it happens

Trigger: Calling @tool with an unsupported first positional argument, e.g. @tool(SomeClass), @tool(fn()) (passing a result instead of the function), @tool({'name': ...}), or a non-string non-callable object.

Common situations: Passing a called function's return value instead of the callable; attempting to pass a description or return type positionally; wrapping functions in functools.partial and expecting tool to accept it positionally.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/7d68590c9f913f48. Report an issue: GitHub.