AI Agent Tool Design Patterns: Perception Tools Explained

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, where a named FastMCP instance collects all perception capabilities.

Server Initialization Pattern


# 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


# 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:


# 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 demonstrates production integration:


# 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


# 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. This module implements a programmatic schema enrichment pattern:


# 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:

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 module establishes common data structures across all tools:


# 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 or an imported file
  3. Register by importing or calling the registration function

Custom Tool Example


# 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, 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 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →