Skip to content

Tools and MCP

Learn: Give the agent tools. Concepts: Tool calls. Guides: Tools on objects and from data, Use MCP servers.

tool

tool(fn: Callable[..., Any]) -> FunctionTool
tool(*, name: str | None = None, description: str | None = None, parallel: bool = True, exception_handler: Callable[[Any], str] | None = None, hints_for: Callable[[dict[str, Any]], Hints | None] | None = None, read_only: bool | None = None, destructive: bool | None = None, idempotent: bool | None = None, open_world: bool | None = None) -> Callable[[Callable[..., Any]], FunctionTool]
tool(fn: Callable[..., Any] | None = None, /, *, name: str | None = None, description: str | None = None, parallel: bool = True, exception_handler: Callable[[Any], str] | None = None, hints_for: Callable[[dict[str, Any]], Hints | None] | None = None, read_only: bool | None = None, destructive: bool | None = None, idempotent: bool | None = None, open_world: bool | None = None) -> FunctionTool | Callable[[Callable[..., Any]], FunctionTool]

Turns a function or method into a tool the model can call.

Type hints become the input schema and the docstring becomes the description. The Args: section of the docstring describes each parameter. A parameter typed State is hidden from the model and receives the current State. The return value is sent to the model: a str as is, None as (done), an Image (or a list of text and Images, such as ["Page loaded", Image(png)]) as an image, anything else as JSON. In such a list a str is text and anything else that is not an Image is sent as JSON text. A value that cannot become JSON raises TypeError, which stops the run like any exception from a tool.

Every parameter needs a type hint. Supported: str, int, float, bool, Literal[...], Enum, list[T], dict[str, T], T | None, dataclasses, TypedDict (on Python 3.11 import it from typing_extensions) and Pydantic models. Parameters with defaults are optional. A mistake in the function raises TypeError when @tool runs, before any model request. tool.spec shows exactly what the model sees.

Use it bare (@tool) or with options (@tool(name=..., description=..., parallel=..., exception_handler=..., hints_for=..., read_only=..., destructive=..., idempotent=..., open_world=...)).

An exception from the tool stops the run, because it is usually a bug. For a failure the model should see and handle, raise ToolError("message"), or name the exceptions in exception_handler.

read_only, destructive, idempotent and open_world describe what the tool does, with the meaning of MCP tool annotations. The model does not see them: they are for your code, such as a loop that asks before calls that are not read-only, and it reads them through agent.tool_map. A hint left out assumes the worst: not read-only, destructive, not idempotent, open world. @tool(read_only=True, open_world=False) fits a tool that reads local files. These hints are the worst case for any call; hints_for= tells what one call does.

Example
@tool
def read_file(path: str) -> str:
    """Read a file's contents

    Args:
        path: Path relative to the repository root
    """
    return Path(path).read_text()

Parameters:

Name Type Description Default
fn Callable[..., Any] | None

The function or method. Every parameter needs a type hint.

None
name str | None

The tool name. Must match [A-Za-z0-9_-]{1,64}. Defaults to the function name.

None
description str | None

Defaults to the docstring's first paragraph.

None
parallel bool

False runs the tool alone after the turn's other calls finish.

True
exception_handler Callable[[Any], str] | None

A function that takes one exception and returns the message the model is told. The type hint of its parameter names the exceptions it takes (httpx.HTTPError, or FileNotFoundError | PermissionError); those become error results and the run continues. Other exceptions stop the run. To let one of them stop the run anyway, raise it in the handler.

def http_errors(error: httpx.HTTPError) -> str:
    return f"request failed: {error}"

@tool(exception_handler=http_errors)
def fetch_url(url: str) -> str:
    return httpx.get(url).raise_for_status().text
None
hints_for Callable[[dict[str, Any]], Hints | None] | None

A function that takes the arguments of one call and returns its Hints, or None when unsure (the tool's own hints then apply). tool.hints_for(args) calls it. The arguments are parsed from JSON but not yet checked against the type hints, so read them with care (args.get(...), isinstance) and return None for anything unexpected. Hints are facts about the call, not a decision to allow it.

def bash_hints(args):
    if args.get("command") in ("ls", "pwd"):
        return Hints(read_only=True)
    return None

@tool(hints_for=bash_hints)
def bash(command: str) -> str: ...
None
read_only bool | None

The tool does not change its environment. Sets destructive=False and idempotent=True.

None
destructive bool | None

The tool may delete or overwrite something (False for one that only adds).

None
idempotent bool | None

Calling it again with the same arguments has no further effect.

None
open_world bool | None

The tool reaches outside its own domain, such as the web, a shell or another system.

None

Returns:

Type Description
FunctionTool | Callable[[Callable[..., Any]], FunctionTool]

A FunctionTool, or a decorator that makes one when options are given. For a tool that is not a Python

FunctionTool | Callable[[Callable[..., Any]], FunctionTool]

function, subclass Tool instead.

Raises:

Type Description
TypeError

@tool is applied twice or to something that is not a function, the function cannot be a tool, exception_handler is not a function with one parameter whose type hint names Exception subclasses, hints_for is not a regular function with one parameter, or a hint is not a bool (see FunctionTool).

ValueError

read_only=True with destructive=True or idempotent=False.

Tool

Tool(*, name: str, description: str = '', input_schema: Mapping[str, Any] | None = None, parallel: bool = True, read_only: bool | None = None, destructive: bool | None = None, idempotent: bool | None = None, open_world: bool | None = None)

A tool the model can call. @tool makes one from a function; subclass it for a tool that is not a Python function, such as one built from a JSON Schema in a database or an OpenAPI spec.

A subclass calls super().__init__(...) with the name, description and input schema, and implements run. run gets the model's arguments as a dict and returns the result, which the model gets as @tool results are: a str as is, None as (done), an Image (or a list with one) as an image, anything else as JSON. It signals with exceptions:

  • ToolInputError("..."): the arguments are wrong. The model gets (input error: ...).
  • ToolError("..."): a failure the model should handle. The model gets it as an error result.
  • Any other exception stops the run.

A subclass that does not call super().__init__, or does not define run, raises TypeError when the Agent is created, and so does passing the class instead of an object. run gets the model's arguments as your own copy: changing it does not change the recorded call, whose args are read-only. A Tool and a FunctionTool (made by @tool) are both Tool objects, and so is an MCP server's MCPTool, so every value of agent.tool_map is a Tool.

Arguments that are not valid JSON, or that were cut off by the output token limit, never reach run: the model gets an input error. run can be async def; agent.arun then awaits it on the event loop, and a regular run runs on a worker thread.

Example
class Webhook(Tool):
    def __init__(self, row):
        super().__init__(
            name=row.name,
            description=row.description,
            input_schema=row.schema,  # {"type": "object", "properties": {...}}
            open_world=True,
        )
        self.url = row.url

    def run(self, args: dict, state: State) -> dict:
        if "title" not in args:
            raise ToolInputError("title is required")
        response = httpx.post(self.url, json=args)
        if response.status_code >= 400:
            raise ToolError(f"HTTP {response.status_code}")
        return response.json()

Sets what the model sees and what the tool does. A subclass calls it from its own __init__.

Parameters:

Name Type Description Default
name str

The name the model calls. Must match [A-Za-z0-9_-]{1,64}.

required
description str

What the model reads about the tool.

''
input_schema Mapping[str, Any] | None

The JSON Schema of the arguments, an object schema such as {"type": "object", "properties": {"title": {"type": "string"}}, "required": ["title"]}. Leave it out for a tool without arguments.

None
parallel bool

False runs the tool alone after the turn's other calls finish.

True
read_only bool | None

The tool does not change its environment. Sets destructive=False and idempotent=True.

None
destructive bool | None

The tool may delete or overwrite something.

None
idempotent bool | None

Calling it again with the same arguments has no further effect.

None
open_world bool | None

The tool reaches outside its own domain, such as the web, a shell or another system.

None

Raises:

Type Description
TypeError

The name is invalid, description is not a string, input_schema is not an object schema, or parallel or a hint is not a bool.

ValueError

read_only=True with destructive=True or idempotent=False.

name instance-attribute

name: str = _check_tool_name(name, explicit=True)

The name the model calls.

description instance-attribute

description: str = description

What the model reads about the tool.

input_schema instance-attribute

input_schema: dict[str, Any] = _check_input_schema(self.name, input_schema)

The JSON Schema object of the arguments, shown to the model.

parallel instance-attribute

parallel: bool = parallel

True runs the tool together with the turn's other calls. False runs it alone after they finish.

read_only instance-attribute

read_only: bool

The tool does not change its environment. False unless given. The model does not see the hints; they are for your own code, such as a loop that asks before calls that are not read-only. These four are the worst case for the tool; hints_for(args) gives the hints of one call.

destructive instance-attribute

destructive: bool

The tool may delete or overwrite something. True unless given, and False for a read-only tool.

idempotent instance-attribute

idempotent: bool

Calling it again with the same arguments has no further effect. False unless given, and True for a read-only tool.

open_world instance-attribute

open_world: bool

The tool reaches outside its own domain, such as the web, a shell or another system. True unless given.

spec property

spec: ToolSpec

The tool as the model sees it: ToolSpec(name, description, input_schema).

is_async property

is_async: bool

True if run is async def.

run

run(args: dict[str, Any], state: State) -> Any

Runs one call. A subclass implements it, as def or async def.

Parameters:

Name Type Description Default
args dict[str, Any]

The model's arguments, parsed from JSON. Not checked against input_schema: check what you rely on, and raise ToolInputError when it is wrong.

required
state State

The State of the run.

required

Returns:

Type Description
Any

The result: a str as is, None as (done), an Image or a list of text and Images as

Any

they are, anything else as JSON.

Raises:

Type Description
ToolInputError

The arguments are wrong. The model gets (input error: ...).

ToolError

A failure the model should handle. The model gets it as an error result.

hints_for

hints_for(args: Mapping[str, Any]) -> Hints

The hints of one call: what running the tool with these arguments does. By default the tool's own hints (read_only and the others), which are the worst case for any call.

A subclass can override it to tell more about one call, such as a shell tool whose ls changes nothing. Return the tool's own hints (super().hints_for(args)) when unsure.

Parameters:

Name Type Description Default
args Mapping[str, Any]

The model's arguments, parsed from JSON but not yet checked against input_schema: any value can be missing or of the wrong type.

required

Returns:

Type Description
Hints

A Hints, or an instance of a Hints subclass that carries more facts about the call.

FunctionTool

FunctionTool(fn: Callable[..., Any], *, name: str | None = None, description: str | None = None, parallel: bool = True, exception_handler: Callable[[Any], str] | None = None, hints_for: Callable[[dict[str, Any]], Hints | None] | None = None, read_only: bool | None = None, destructive: bool | None = None, idempotent: bool | None = None, open_world: bool | None = None)

Bases: Tool

A tool made by @tool from a function or method. Calling it runs the original function as is, without validation.

A @tool method in a class becomes a bound tool when accessed on an object (fs.read_file). The attributes do not change after creation.

Usually created with @tool. A bad tool raises here, before any model call.

Parameters:

Name Type Description Default
fn Callable[..., Any]

The function or method. Every parameter needs a type hint.

required
name str | None

The tool name. Must match [A-Za-z0-9_-]{1,64}. Defaults to the function name.

None
description str | None

Defaults to the docstring's first paragraph.

None
parallel bool

False runs the tool alone after the turn's other calls finish.

True
exception_handler Callable[[Any], str] | None

See tool.

None
hints_for Callable[[dict[str, Any]], Hints | None] | None

See tool.

None
read_only bool | None

See tool.

None
destructive bool | None

See tool.

None
idempotent bool | None

See tool.

None
open_world bool | None

See tool.

None

Raises:

Type Description
TypeError

The name is invalid, a parameter has no type hint or an unsupported type, the function takes *args, **kwargs or positional-only parameters, exception_handler is not a function with one parameter whose type hint names Exception subclasses, hints_for is not a regular function with one parameter, or a hint is not a bool.

ValueError

read_only=True with destructive=True or idempotent=False.

input_schema instance-attribute

input_schema: dict[str, Any]

The JSON Schema object shown to the model. State parameters and self are left out, Args: descriptions become property descriptions, and parameters with defaults are optional.

exception_handler instance-attribute

exception_handler: Callable[[Any], str] | None = exception_handler

Turns the exceptions named in its parameter's type hint into error results the model sees. None if not given.

is_async property

is_async: bool

True if the original function is async def.

run

run(args: dict[str, Any], state: State) -> Any

Checks the arguments against the type hints (ToolInputError if they do not fit), then calls the function with them and the State. Returns a coroutine for an async def function.

hints_for

hints_for(args: Mapping[str, Any]) -> Hints

The hints of one call: what the hints_for= function returns for args, or the tool's own hints when it returns None or was not given.

Parameters:

Name Type Description Default
args Mapping[str, Any]

The model's arguments, parsed from JSON but not yet checked against the type hints.

required

Returns:

Type Description
Hints

A Hints, or an instance of a Hints subclass.

Raises:

Type Description
TypeError

The hints_for= function returned something other than Hints or None.

copy

copy(**changes: Any) -> FunctionTool

Returns a new tool with some options changed. The original does not change, and a bound tool stays bound.

Parameters:

Name Type Description Default
**changes Any

Any @tool option: name, description, parallel, exception_handler, hints_for, read_only, destructive, idempotent or open_world. hints_for=None removes that function, and exception_handler=None removes the handler (the Agent itself does not know about handlers).

{}

Returns:

Type Description
FunctionTool

The new tool.

Raises:

Type Description
TypeError

A name in changes is not an option, or a new value fails the same checks as @tool.

ValueError

The new hints contradict each other, as in @tool.

Example
strict_fetch = fetch_url.copy(exception_handler=None)  # every exception stops the run

Hints dataclass

Hints(*, read_only: bool = None, destructive: bool = None, idempotent: bool = None, open_world: bool = None)

The four hints of one call: what running the tool with these arguments does. tool.hints_for(args) returns them.

Made like the hints of @tool: Hints(read_only=True, open_world=False). A hint left out assumes the worst (not read-only, destructive, not idempotent, open world), and read_only=True sets destructive=False and idempotent=True. After construction every hint is a bool.

A subclass can carry more facts about the call. Give each new field a default:

@dataclass(frozen=True)
class FileHints(Hints):
    paths: tuple[str, ...] = ()

Raises:

Type Description
TypeError

A hint is not a bool.

ValueError

read_only=True with destructive=True or idempotent=False.

read_only class-attribute instance-attribute

read_only: bool = None

The call does not change its environment.

destructive class-attribute instance-attribute

destructive: bool = None

The call may delete or overwrite something.

idempotent class-attribute instance-attribute

idempotent: bool = None

Making the call again with the same arguments has no further effect.

open_world class-attribute instance-attribute

open_world: bool = None

The call reaches outside the tool's own domain, such as the web, a shell or another system.

MCP

MCP(command: str | None = None, *, name: str | None = None, url: str | None = None, server: Any = None, env: Mapping[str, str] | None = None, headers: Mapping[str, str] | None = None, cwd: str | None = None, timeout: float | None = None)

An MCP server, put in Agent(tools=[...]) like any other tool. Needs pip install "alpineagents[mcp]".

The model sees the server's tools as {name}__{tool}, e.g. github__create_issue. To give the Agent only one tool, pass gh.search_code, or gh["search-code"] for a name that is not a Python identifier.

agent.run connects the servers and disconnects them when it ends. with agent: (or async with agent:) keeps them connected across runs. Agents and concurrent runs that share one MCP object share one connection.

Example
github = MCP("npx -y @modelcontextprotocol/server-github", name="github", env={"GITHUB_TOKEN": token})
linear = MCP(url="https://mcp.linear.app/mcp", name="linear", headers={"Authorization": f"Bearer {key}"})
agent = Agent(model="claude-sonnet-5", tools=[github, linear.list_issues])

Takes exactly one of command, url or server. Nothing connects until the Agent uses it.

Parameters:

Name Type Description Default
command str | None

The command that starts a stdio server.

None
name str | None

Required. Letters, digits, - and single _. Prefixes the tool names.

None
url str | None

The URL of a Streamable HTTP server.

None
server Any

Anything mcp.Client accepts, such as an in-process server in tests.

None
env Mapping[str, str] | None

Extra environment variables for command, added to the MCP SDK's safe default environment.

None
headers Mapping[str, str] | None

HTTP headers for url, e.g. {"Authorization": "Bearer ..."}.

None
cwd str | None

The working directory for command.

None
timeout float | None

Seconds to wait for connecting and for each call. None waits without a limit.

None

Raises:

Type Description
TypeError

No server or more than one is given, env without command, or headers without url.

ValueError

name is missing or not allowed, or command is empty.

name instance-attribute

name = name

The server name that prefixes its tool names.

command instance-attribute

command = command

The command that starts a stdio server, or None.

url instance-attribute

url = url

The URL of a Streamable HTTP server, or None.

server instance-attribute

server = server

The object given as server=, or None.

env instance-attribute

env = dict(env) if env is not None else None

Extra environment variables for command, or None.

headers instance-attribute

headers = dict(headers) if headers is not None else None

HTTP headers for url, or None.

cwd instance-attribute

cwd = cwd

The working directory for command, or None.

timeout instance-attribute

timeout = timeout

Seconds to wait for connecting and for each call, or None for no limit.

MCPTool

MCPTool(server: MCP, remote: Any)

Bases: Tool

A tool of a connected MCP server, as found in agent.tool_map under {server}__{tool}.

It has the same name, description, input_schema and hints as any Tool. The hints come from the server's tool annotations (readOnlyHint and so on), with the same defaults as @tool for the ones it leaves out. They are what the server says about itself, not a guarantee: do not trust them more than the server. hints_for(args) returns the same hints for every call, and AllowByReadOnly() ignores them unless it has trust_mcp=True.

server instance-attribute

server: MCP = server

The MCP server the tool belongs to.

remote_name instance-attribute

remote_name: str = remote.name

The tool's name on the server (name has the {server}__ prefix).

read_only instance-attribute

read_only: bool

From readOnlyHint. False if the server does not say.

destructive instance-attribute

destructive: bool

From destructiveHint. True if the server does not say, and False for a read-only tool.

idempotent instance-attribute

idempotent: bool

From idempotentHint. False if the server does not say, and True for a read-only tool.

open_world instance-attribute

open_world: bool

From openWorldHint. True if the server does not say.

run async

run(args: dict[str, Any], state: Any) -> str | list[str | Image]

Calls the tool on the server. Missing required arguments are an input error; the server checks the rest. An error the server reports (isError, or an error response) becomes an error result; a lost connection raises MCPConnectionError.

Text blocks are joined by newlines, image blocks become Images (the result is then a list of text and images), other blocks become short placeholders, structured_content is sent as JSON when there are no blocks, and (done) when there is nothing.