Skip to content
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,40 @@ agent["greeter"].send("Good Evening!") # Dictionary access is supported
)
```

### Function Tools

Register Python functions as tools directly in code — no MCP server or external file needed. Both sync and async functions are supported. The function name and docstring are used as the tool name and description by default, or you can override them with `name=` and `description=`.

**Per-agent tools (`@agent.tool`)** — scope a tool to a specific agent:

```python
@fast.agent(name="writer", instruction="You write things.")
async def writer(): ...

@writer.tool
def translate(text: str, language: str) -> str:
"""Translate text to the given language."""
return f"[{language}] {text}"

@writer.tool(name="summarize", description="Produce a one-line summary")
def summarize(text: str) -> str:
return f"Summary: {text[:80]}..."
```

**Global tools (`@fast.tool`)** — available to all agents that don't declare their own tools:

```python
@fast.tool
def get_weather(city: str) -> str:
"""Return the current weather for a city."""
return f"Sunny in {city}"

@fast.agent(name="assistant", instruction="You are helpful.")
# assistant gets get_weather (global @fast.tool)
```

Agents with `@agent.tool` or `function_tools=` only see their own tools — globals are not injected. Use `function_tools=[]` to explicitly opt out of globals with no tools.

### Multimodal Support

Add Resources to prompts using either the inbuilt `prompt-server` or MCP Types directly. Convenience class are made available to do so simply, for example:
Expand Down
35 changes: 35 additions & 0 deletions examples/function-tools/basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
Basic @fast.tool example.

Register Python functions as tools using the @fast.tool decorator.
Tools are automatically available to all agents.

Run with: uv run examples/function-tools/basic.py
"""

import asyncio

from fast_agent import FastAgent

fast = FastAgent("Function Tools Example")


@fast.tool
def get_weather(city: str) -> str:
"""Return the current weather for a city."""
return f"Currently sunny and 22°C in {city}"


@fast.tool(name="add", description="Add two numbers together")
def add_numbers(a: int, b: int) -> int:
return a + b


@fast.agent(instruction="You are a helpful assistant with access to tools.")
async def main() -> None:
async with fast.run() as agent:
await agent.interactive()


if __name__ == "__main__":
asyncio.run(main())
60 changes: 60 additions & 0 deletions examples/function-tools/scoping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
@agent.tool scoping example.

Demonstrates how tools can be scoped to individual agents using
@agent_func.tool, and how @fast.tool broadcasts globally.

Run with: uv run examples/function-tools/scoping.py
"""

import asyncio

from fast_agent import FastAgent

fast = FastAgent("Tool Scoping Example")


@fast.agent(
name="writer",
instruction="You are a writing assistant with translation and summarization tools.",
default=True,
)
async def writer() -> None:
pass


@fast.agent(
name="analyst",
instruction="You analyse text. You can only count words.",
)
async def analyst() -> None:
pass


@writer.tool
def translate(text: str, language: str) -> str:
"""Translate text to the given language."""
return f"[{language}] {text}"


@writer.tool
def summarize(text: str) -> str:
"""Produce a one-line summary."""
return f"Summary: {text[:80]}..."


@analyst.tool(name="word_count", description="Count words in text")
def count_words(text: str) -> int:
"""Count the number of words in text."""
return len(text.split())


async def main() -> None:
async with fast.run() as agent:
# "writer" sees translate and summarize (its own @writer.tool tools)
# "analyst" sees only word_count (its own @analyst.tool tool)
await agent.interactive()


if __name__ == "__main__":
asyncio.run(main())
28 changes: 22 additions & 6 deletions src/fast_agent/agents/agent_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,16 @@ class AgentType(StrEnum):
# Function tools can be:
# - A callable (Python function)
# - A string spec like "module.py:function_name" (for dynamic loading)
FunctionToolConfig: TypeAlias = Callable[..., Any] | str
@dataclass(frozen=True)
class ScopedFunctionToolConfig:
"""A single local Python tool registration with scoped metadata."""

function: Callable[..., Any]
name: str | None = None
description: str | None = None


FunctionToolConfig: TypeAlias = Callable[..., Any] | str | ScopedFunctionToolConfig

FunctionToolsConfig: TypeAlias = list[FunctionToolConfig] | None

Expand All @@ -69,16 +78,23 @@ class MCPConnectTarget:

@dataclass
class AgentConfig:
"""Configuration for an Agent instance"""
"""Configuration for an Agent instance.

Naming note:
- ``tools`` filters MCP-discovered tools by server name.
- ``function_tools`` configures local Python function tools.
- Runtime constructors such as ``ToolAgent(..., tools=...)`` use ``tools``
for the resolved executable function-tool objects, not these MCP filters.
"""

name: str
instruction: str = DEFAULT_AGENT_INSTRUCTION
description: str | None = None
tool_input_schema: dict[str, Any] | None = None
servers: list[str] = field(default_factory=list)
tools: dict[str, list[str]] = field(default_factory=dict) # filters for tools
resources: dict[str, list[str]] = field(default_factory=dict) # filters for resources
prompts: dict[str, list[str]] = field(default_factory=dict) # filters for prompts
tools: dict[str, list[str]] = field(default_factory=dict) # MCP tool filters by server
resources: dict[str, list[str]] = field(default_factory=dict) # MCP resource filters by server
prompts: dict[str, list[str]] = field(default_factory=dict) # MCP prompt filters by server
skills: SkillConfig = SKILLS_DEFAULT
skill_manifests: list[SkillManifest] = field(default_factory=list, repr=False)
model: str | None = None
Expand All @@ -90,7 +106,7 @@ class AgentConfig:
tool_only: bool = False
elicitation_handler: ElicitationFnT | None = None
api_key: str | None = None
function_tools: FunctionToolsConfig = None
function_tools: FunctionToolsConfig = None # Local Python function tools
shell: bool = False
cwd: Path | None = None
tool_hooks: ToolHooksConfig = None
Expand Down
13 changes: 13 additions & 0 deletions src/fast_agent/agents/tool_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ class ToolAgent(LlmAgent, _ToolLoopAgent):
Pass either:
- native FastMCP FunctionTool objects
- regular Python functions (wrapped as FunctionTools)

Naming note:
``tools`` here means executable local/function tools available to the
agent. It does not refer to ``AgentConfig.tools``, which is the MCP
filter map used by ``McpAgent``.
"""

def __init__(
Expand All @@ -100,6 +105,14 @@ def __init__(
tools: Sequence[FunctionTool | Callable[..., Any]] = (),
context: Context | None = None,
) -> None:
"""Create a tool-capable agent.

Args:
config: Agent configuration. ``config.tools`` remains the MCP
filter map; it is separate from this ``tools`` argument.
tools: Executable local/function tools to expose on the agent.
context: Optional runtime context.
"""
super().__init__(config=config, context=context)

self._execution_tools: dict[str, FunctionTool] = {}
Expand Down
Loading
Loading