# AI Agent Tool Design Patterns: Perception Tools Explained

> Explore AI agent tool design patterns focusing on perception tools. Learn how FastMCP architecture enables dynamic tool discovery for seamless interaction with web search, file systems, and APIs.

- Repository: [Bojie Li/ai-agent-book](https://github.com/bojieli/ai-agent-book)
- Tags: deep-dive
- Published: 2026-08-01

---

**Perception tools in AI agents follow a FastMCP-based architecture where async functions decorated with `@mcp.tool` expose declarative schemas, enabling dynamic tool discovery and modular capability expansion.** This pattern separates tool declaration from implementation, allowing agents to perceptually interact with web search, file systems, and external APIs through a unified interface.

The `bojieli/ai-agent-book` repository demonstrates this design in Chapter 4's perception-tools package, a production-ready reference for building extensible agent capabilities. According to the source code, the implementation centers on a Micro-Control-Protocol (MCP) server that bridges LLM reasoning with real-world data acquisition.

## Core Architecture: FastMCP Server Foundation

The perception-tools package builds on **FastMCP**, a high-level MCP server implementation that handles tool registration, request routing, and JSON response formatting. The server lifecycle begins in [`chapter4/perception-tools/src/main.py`](https://github.com/bojieli/ai-agent-book/blob/main/chapter4/perception-tools/src/main.py), where a named FastMCP instance collects all perception capabilities.

### Server Initialization Pattern

```python

# chapter4/perception-tools/src/main.py – server instantiation

from mcp.server import FastMCP
from pydantic import Field

mcp = FastMCP(
    "perception-tools",
    instructions="""
    Perception Tools MCP Server
    Provides capabilities for the AI agent to understand the environment...
    """
)

```

The `instructions` parameter serves dual purpose: **documentation for human developers** and **context grounding for the LLM** when selecting tools. The server name `"perception-tools"` becomes the canonical identifier for agent-side connections.

## Tool Declaration: The @mcp.tool Decorator Pattern

The `@mcp.tool` decorator transforms ordinary async functions into **self-describing agent capabilities**. Each declaration combines:

- **Human-readable description** — guides the LLM's tool selection reasoning
- **Pydantic-typed parameters** — automatic validation with `Field` metadata
- **Async implementation** — non-blocking execution for concurrent tool calls

### Web Search Tool Example

```python

# chapter4/perception-tools/src/main.py – tool definition pattern

@mcp.tool(description="Search the web using DuckDuckGo (free, no API key required)")
async def web_search(
    query: str = Field(description="Search query string"),
    num_results: int = Field(default=5, description="Number of results (1-10)"),
    region: str = Field(default="wt-wt", description="Region code"),
    source: str = Field(default="news", description="news or general")
):
    """Execute web search through DuckDuckGo."""
    return await search_web(query, num_results, region, source)

```

Key implementation details from the source:

- `Field(default=...)` provides **sensible defaults** while allowing LLM override
- `description` parameters in `Field()` populate the **JSON Schema** sent to the LLM
- The actual implementation delegates to `search_web()` in a separate module, maintaining **separation of concerns**

## Tool Implementation Modules: Pure Async Functions

The architecture enforces **strict separation between declaration and implementation**. Tool logic resides in focused modules imported at the top of [`main.py`](https://github.com/bojieli/ai-agent-book/blob/main/main.py):

```python

# chapter4/perception-tools/src/main.py – line 17-38 imports

from search_tools import search_web, search_news, search_academic
from document_tools import read_document, summarize_document, extract_tables
from multimodal_tools import analyze_image, transcribe_audio, parse_video
from filesystem_tools import list_directory, read_file, grep_files
from public_data_tools import get_weather, get_stock_price, get_exchange_rate
from private_data_tools import query_database, access_internal_api

```

This modular structure enables **independent testing**, **team parallelization**, and **selective deployment** — agents can import only the capability subsets they require.

## Agent-Side Integration: MCPManager Connection

Agents consume perception tools through an **MCP client manager** that handles server lifecycle and tool invocation. The [`chapter4/agent-with-event-trigger/agent.py`](https://github.com/bojieli/ai-agent-book/blob/main/chapter4/agent-with-event-trigger/agent.py) demonstrates production integration:

```python

# chapter4/agent-with-event-trigger/agent.py – connection setup

from mcp_manager import MCPManager

class EventDrivenAgent:
    async def initialize(self):
        self.mcp_manager = MCPManager()
        await self.mcp_manager.connect_server(
            name="perception",
            path=os.path.join(
                agent_dir, 
                self.config.mcp_perception_tools_path  # points to main.py

            )
        )

```

The `connect_server()` call establishes:
- **Process spawning** for the MCP server
- **STDIO or HTTP transport** negotiation
- **Tool catalog discovery** — the client receives all available tool schemas

### Runtime Tool Invocation

```python

# chapter4/agent-with-event-trigger/agent.py – execution loop

async def process_query(self, user_question: str):
    # LLM generates tool_call: {"name": "web_search", "arguments": {...}}

    tool_result = await self.mcp_manager.call_tool(
        "web_search",
        {"query": user_question, "num_results": 5, "source": "news"}
    )
    
    # Result injected into conversation context

    self.conversation.append({
        "role": "assistant",
        "tool_result": tool_result,
        "tool_name": "web_search"
    })
    
    # LLM generates final response incorporating tool output

    return await self.llm.generate(self.conversation)

```

The `call_tool()` method handles:
- **JSON serialization** of arguments
- **Transport-layer communication** (HTTP POST or STDIO)
- **Response parsing** and error recovery
- **Timeout and cancellation** management

## Expanded Catalog: Scaling to 120+ Tools

For research experiments requiring extensive capability coverage, the repository includes [`chapter4/perception-tools/src/expanded_catalog.py`](https://github.com/bojieli/ai-agent-book/blob/main/chapter4/perception-tools/src/expanded_catalog.py). This module implements a **programmatic schema enrichment** pattern:

```python

# chapter4/perception-tools/src/expanded_catalog.py – concept

from mcp.server import FastMCP

def register_expanded_catalog(mcp: FastMCP) -> None:
    """Register 120+ additional tool definitions for large-scale experiments."""
    
    catalog = load_external_tool_definitions()  # YAML/JSON source

    
    for tool_def in catalog:
        # Dynamically construct async function from schema

        impl = create_tool_implementation(tool_def)
        mcp.tool(
            description=tool_def.description,
            name=tool_def.name
        )(impl)

```

**Key insight**: The expanded catalog demonstrates that **tool schemas can outpace implementations** — useful for:
- Prototyping agent behavior before building infrastructure
- A/B testing tool availability without code changes
- Declarative capability management through configuration

## Direct HTTP Client: Bypassing the Agent Framework

For debugging or standalone usage, tools can be invoked directly via HTTP:

```python
import json
import aiohttp

async def call_perception_tool(name: str, arguments: dict) -> dict:
    """Direct MCP server invocation without agent framework."""
    payload = {
        "tool_call": {
            "name": name,
            "arguments": arguments
        },
        "messages": []  # Optional conversation context

    }
    
    async with aiohttp.ClientSession() as session:
        async with session.post(
            "http://localhost:8000/tool",
            json=payload
        ) as response:
            return await response.json()

# Usage: retrieve current weather

result = await call_perception_tool(
    "get_weather",
    {"location": "San Francisco", "units": "celsius"}
)
print(json.dumps(result, indent=2))

```

This pattern enables **integration testing**, **performance benchmarking**, and **third-party service consumption** of the perception layer.

## Base Types and Shared Utilities

The [`chapter4/perception-tools/src/base.py`](https://github.com/bojieli/ai-agent-book/blob/main/chapter4/perception-tools/src/base.py) module establishes **common data structures** across all tools:

```python

# chapter4/perception-tools/src/base.py – core types

from pydantic import BaseModel
from typing import Any, Optional, Literal

class ToolResponse(BaseModel):
    """Standardized wrapper for all tool outputs."""
    status: Literal["success", "error", "partial"]
    data: Any
    metadata: dict[str, Any] = {}
    error_message: Optional[str] = None

class PerceptionContext(BaseModel):
    """Shared runtime context passed to tools."""
    session_id: str
    user_id: Optional[str] = None
    permissions: list[str] = []
    rate_limit_remaining: Optional[int] = None

```

These base types ensure **consistent error handling**, **observability integration**, and **access control propagation** throughout the perception layer.

## Extending the Pattern: Adding Custom Tools

New perception capabilities follow a **three-step registration pattern**:

1. **Implement** the async function in a dedicated module
2. **Decorate** with `@mcp.tool()` in [`main.py`](https://github.com/bojieli/ai-agent-book/blob/main/main.py) or an imported file
3. **Register** by importing or calling the registration function

### Custom Tool Example

```python

# chapter4/perception-tools/src/crypto_tools.py

from pydantic import Field
from mcp.server import mcp  # Shared FastMCP instance

@mcp.tool(description="Fetch cryptocurrency price from CoinGecko API")
async def get_crypto_price(
    symbol: str = Field(description="Cryptocurrency symbol, e.g. 'bitcoin'"),
    currency: str = Field(default="usd", description="Fiat currency code")
) -> dict:
    """Real-time crypto price lookup with caching."""
    cache_key = f"crypto:{symbol}:{currency}"
    
    if cached := await cache.get(cache_key):
        return {"price": cached, "source": "cache"}
    
    price = await fetch_coingecko(symbol, currency)
    await cache.set(cache_key, price, ttl=60)
    
    return {"price": price, "source": "api", "currency": currency}

```

After adding `from crypto_tools import get_crypto_price` to [`main.py`](https://github.com/bojieli/ai-agent-book/blob/main/main.py), the tool becomes immediately discoverable without server restart (when using hot-reload development mode).

## Summary

The perception-tools design pattern in `bojieli/ai-agent-book` establishes a **robust, scalable foundation** for AI agent capabilities:

- **FastMCP server** provides transport, routing, and protocol compliance through a clean decorator-based API
- **Declaration-implementation separation** enables parallel development and independent testing of perception modules
- **Modular catalog structure** supports both focused deployments and expanded experimental configurations
- **MCPManager client integration** gives agents seamless tool discovery and invocation with full lifecycle management
- **Pydantic schemas** ensure type safety, automatic validation, and self-documenting tool interfaces

This architecture directly addresses the core challenge of **grounding LLM reasoning in real-world data** while maintaining system maintainability as capability requirements grow.

## Frequently Asked Questions

### What is the difference between perception tools and action tools in AI agents?

**Perception tools acquire information from the environment** — reading files, searching the web, querying databases, analyzing images. **Action tools modify the environment** — sending emails, updating records, triggering deployments. The `bojieli/ai-agent-book` repository organizes these into separate Chapter 4 packages: `perception-tools` for information gathering and `action-tools` for state changes. Both use identical FastMCP infrastructure but follow different safety patterns — perception tools are generally read-only and cacheable, while action tools require confirmation flows and idempotency checks.

### How does the MCP protocol compare to function calling in OpenAI's API?

**MCP (Micro-Control-Protocol) is transport and vendor-agnostic**, designed for local or networked tool servers that multiple agents can share. OpenAI function calling embeds tool schemas directly in the chat completion request and executes registered Python functions within the same process. The `chapter4/perception-tools` implementation can expose the same capabilities through both interfaces: the FastMCP server handles MCP clients, while a thin wrapper can adapt the tool functions for OpenAI's `functions` parameter. MCP excels in multi-agent environments and language-agnostic deployments; native function calling reduces latency for single-process Python agents.

### Can perception tools handle streaming or real-time data sources?

**Yes, through async generators and Server-Sent Events**. While the base `ToolResponse` pattern assumes discrete request-response cycles, the FastMCP server in [`main.py`](https://github.com/bojieli/ai-agent-book/blob/main/main.py) can be extended with streaming endpoints. For real-time sources like WebSocket feeds or log tails, tools return an `async generator` that yields `ToolResponse` chunks, and the MCPManager client consumes these through an `async for` loop. The [`expanded_catalog.py`](https://github.com/bojieli/ai-agent-book/blob/main/expanded_catalog.py) module includes experimental streaming schemas for live data scenarios, though production implementations require additional backpressure and reconnection handling not shown in the reference code.

### How do you secure private data tools in the perception layer?

**Security operates at three layers**: transport encryption (TLS for HTTP MCP connections), authentication (API keys or mTLS in `MCPManager.connect_server()`), and authorization (permission checks within tool implementations). The `private_data_tools` module referenced in [`main.py`](https://github.com/bojieli/ai-agent-book/blob/main/main.py) should validate `PerceptionContext.permissions` against required scopes before executing database queries or internal API calls. The base `ToolResponse` structure supports partial redaction through the `metadata` field for audit logging without exposing sensitive values. For production deployments, run private tools in separate MCP server processes with network isolation from public data tools.