Building Custom Tools for Agents (Native + via MCP)
A hands-on walkthrough building a real-time currency-conversion tool for a CrewAI agent — first as a native BaseTool, then re-implemented as a reusable MCP server consumable by any agent.
Establish Transport pipe connection
Host client (IDE or AI assistant) connects to the server process over standard output streams.
▶📚 Prerequisites(1)
🎓 Learning objectives
- •Build a custom CrewAI tool by subclassing BaseTool with a Pydantic input schema
- •Wire a custom tool into an Agent/Task/Crew and execute it end-to-end
- •Expose the same tool logic as a standalone MCP server using @mcp.tool()
- •Consume a remote MCP tool from a CrewAI agent via MCPServerAdapter
- •Explain when to keep a tool native vs. expose it via MCP
What is it?
This is a concrete, hands-on pattern for extending an agent's capabilities beyond pure LLM reasoning: giving it tools that fetch real-time data, call external APIs, or run computations the model cannot do reliably on its own (like knowing today's exact currency exchange rate). The example built is a currency-conversion tool, implemented twice — first as a tool native to a single CrewAI agent, then re-implemented as a standalone MCP server so the exact same functionality becomes a reusable capability that any agent, in any Crew or framework, can connect to and call.
Why it exists
LLM-powered agents are great at reasoning and generating responses, but they lack direct access to real-time information, external systems, and specialized computations — an LLM asked for today's USD-to-EUR exchange rate will either refuse or hallucinate a plausible-sounding but wrong number. Native tools exist to close this gap for a single agent quickly. The MCP-exposed version exists to solve a follow-on problem: once you've built a genuinely useful tool, re-implementing it inside every new Crew or agent that needs it is wasteful and creates drift (bug fixes applied to one copy don't propagate to the others) — exposing it once via MCP makes it a shared, centrally-maintained capability.
Problem it solves
Native custom tools solve the immediate 'this agent needs a capability the LLM doesn't have' problem for a single project. The MCP-exposed version solves the 'tool sprawl' problem that shows up once you have more than one agent or Crew — instead of N copies of the same currency-conversion logic scattered across N codebases (each needing separate bug fixes and API-key management), you get one server, one source of truth, consumed identically by every agent that needs it.
Intuition
A native tool is like keeping a specialized kitchen gadget in one chef's personal toolkit — convenient for that chef, but every other chef in the restaurant who needs the same gadget has to buy and maintain their own copy. An MCP-exposed tool is like installing that gadget as shared restaurant equipment in the central kitchen — any chef can walk up and use it, it's maintained once, and when it's upgraded, every chef benefits immediately without changing anything in their own workflow.
Analogy
Think of the native tool as a local function you copy-paste into every script that needs it — works fine until you find a bug and have to remember every place you pasted it. The MCP-exposed version is like turning that function into a proper shared library / microservice — fix it once at the source, and every consumer picks up the fix automatically the next time they call it.
Technical explanation
The native implementation has three parts: (1) a Pydantic model defining the tool's expected input fields (e.g., amount: float, from_currency: str, to_currency: str); (2) a CurrencyConverterTool class inheriting from CrewAI's BaseTool, implementing a _run method that CrewAI calls whenever the agent invokes the tool — inside _run, the code makes a live HTTP request to an exchange-rate API, handles request failures and invalid currency codes, and returns a formatted result; (3) wiring — instantiate the tool, pass it into an Agent's tools list, define a Task describing what the agent should do with it, and execute via a Crew.
The MCP-exposed implementation re-uses the same core logic but restructures it as a standalone server: a server.py script initializes an MCP server instance, defines the tool's logic under a function decorated with @mcp.tool() (which takes amount, source currency, and target currency, and returns the converted result using the same real-time exchange-rate API), and starts the server, exposing the tool at a URL like http://localhost:8081/sse. Any CrewAI agent (or agent from a different framework entirely) can then discover and call this tool by connecting through an MCPServerAdapter, which handles the protocol-level communication — from the agent's perspective, calling the remote MCP tool looks identical to calling a locally-defined tool.
Architecture
Native tool architecture: Pydantic input schema → BaseTool subclass (_run method) → Agent (tools=[CurrencyConverterTool()]) → Task → Crew.kickoff() — everything runs in a single process.
MCP-exposed architecture: server.py (standalone process) → @mcp.tool()-decorated function → running MCP server exposed over SSE at a URL → any number of separate agent processes each connect via MCPServerAdapter(server_params) → the adapter discovers the tool's schema at connection time and makes it available to the consuming agent exactly like a native tool.
Workflow
- Design the tool's input schema first using Pydantic — this becomes both the native tool's input validation and (later) the MCP tool's discoverable schema.
- Implement the core logic (the API call, error handling for failed requests and invalid inputs) as a plain function, independent of any framework — this is the piece you'll reuse in both versions.
- (Native) Wrap the logic in a BaseTool subclass with a _run method, attach it to an Agent's tools list, define a Task, and run via a Crew — verify it works for a single agent.
- (MCP) Move the same core logic into a server.py script, decorate it with @mcp.tool(), and start the server — verify it's reachable (e.g., at http://localhost:8081/sse).
- Connect a CrewAI agent to the running MCP server via MCPServerAdapter instead of a local tool instance, and confirm the agent can call it identically.
- Once working, connect a second agent (or a completely different framework's agent) to the same running server to confirm the reuse benefit — one server, multiple independent consumers.
Example
── Native tool (single CrewAI agent) ─────────────────────────────
from pydantic import BaseModel from crewai.tools import BaseTool import requests
class CurrencyInput(BaseModel): amount: float from_currency: str to_currency: str
class CurrencyConverterTool(BaseTool): name: str = 'convert_currency' description: str = 'Convert an amount from one currency to another using live rates' args_schema: type[BaseModel] = CurrencyInput
def _run(self, amount: float, from_currency: str, to_currency: str) -> str:
try:
resp = requests.get(
f'https://v6.exchangerate-api.com/v6/{API_KEY}/pair/'
f'{from_currency}/{to_currency}/{amount}', timeout=5,
)
resp.raise_for_status()
result = resp.json()['conversion_result']
return f'{amount} {from_currency} = {result} {to_currency}'
except Exception as e:
return f'Conversion failed: {e}'
currency_analyst = Agent( role='Currency Analyst', goal='Provide accurate currency conversions', tools=[CurrencyConverterTool()], )
── MCP-exposed version (server.py, standalone) ───────────────────
from mcp.server.fastmcp import FastMCP
mcp = FastMCP('currency-tools')
@mcp.tool() def convert_currency(amount: float, from_currency: str, to_currency: str) -> str: '''Convert an amount from one currency to another using live rates.''' resp = requests.get( f'https://v6.exchangerate-api.com/v6/{API_KEY}/pair/' f'{from_currency}/{to_currency}/{amount}', timeout=5, ) result = resp.json()['conversion_result'] return f'{amount} {from_currency} = {result} {to_currency}'
if name == 'main': mcp.run(transport='sse') # exposes at http://localhost:8081/sse
── Consuming the MCP tool from any CrewAI agent ──────────────────
from crewai_tools import MCPServerAdapter
server_params = {'url': 'http://localhost:8081/sse'} with MCPServerAdapter(server_params) as mcp_tools: agent = Agent(role='Currency Analyst', goal='...', tools=mcp_tools)
Real-world usage
Companies building internal agent platforms (e.g., an internal 'AI tools hub') commonly follow exactly this native-to-MCP migration path: build a tool quickly for one team's agent, then once a second team needs the same capability, extract it into a standalone MCP server so both teams' agents (which may even use different frameworks — CrewAI, LangGraph, custom) consume the same maintained service. Financial services and fintech companies building AI copilots frequently start with exactly this kind of native currency/market-data tool before centralizing it as more products need the same live-data capability. This pattern mirrors the broader software engineering shift from 'copy-pasted utility functions' to 'shared internal microservices' — MCP is simply that same maturation applied to agent tools specifically.
Trade-offs
A native tool is faster to build and has zero deployment/networking overhead — ideal for a single agent, a prototype, or a tool that will genuinely never be reused. An MCP-exposed tool adds real infrastructure (a running server process, a network hop, connection/auth management) but pays off the moment more than one agent or framework needs the same capability — centralizing maintenance, avoiding drift between copies, and making the tool consumable by agents built with entirely different frameworks. The rule of thumb from this course's own two-part structure: build native first to validate the tool works, then extract to MCP once reuse across agents/Crews becomes a real requirement, not a hypothetical one.
Visual explanation
Two diagrams.
Native tool: [CrewAI Agent] directly embeds → [CurrencyConverterTool (BaseTool subclass)] → calls → [ExchangeRate-API] — the tool logic lives inside the same process/codebase as the agent.
MCP-exposed tool: [server.py: MCP Server exposing convert_currency via @mcp.tool()] running standalone at http://localhost:8081/sse → [CrewAI Agent #1] and [CrewAI Agent #2] and [LangGraph Agent #3] each connect independently via MCPServerAdapter → all three call the SAME running server instance, sharing one source of truth for the tool's logic and API key.
Advantages
- —
Native tools are fast to build with zero extra infrastructure — ideal for single-agent prototypes
- —
MCP-exposed tools eliminate code duplication and drift once more than one agent needs the same capability
- —
MCP tools are framework-agnostic — a tool built once can be consumed by CrewAI, LangGraph, or any other MCP-compatible agent
- —
Centralizing a tool as an MCP server means bug fixes and API-key rotation happen in one place, not N scattered copies
Disadvantages
- —
MCP exposure adds real infrastructure: a standalone running server process, network communication, and connection management
- —
Native tools don't scale well the moment a second agent or team needs the identical capability — leads to copy-paste drift
- —
Debugging a remote MCP tool call involves one more network hop and process boundary compared to a local function call
- —
Extracting to MCP prematurely (before any real reuse need exists) adds unnecessary operational overhead for no benefit
Common mistakes
- —
Building native tool logic with framework-specific assumptions baked in, making it hard to later extract into a clean, framework-agnostic MCP server
- —
Not handling API failures and invalid inputs inside the tool logic — a currency converter that crashes on a bad currency code will break the whole agent turn
- —
Extracting every tool to MCP by default 'just in case', adding server/deployment overhead for tools that will only ever be used by one agent
- —
Forgetting that MCP tool schemas need clear descriptions just like native tool docstrings — a vague MCP tool description confuses every consuming agent, not just one
- —
Hardcoding API keys directly in tool code instead of loading them from environment variables — this becomes a bigger liability once the tool is centralized and shared across teams
📂 Subtopics
Defining Tools: Pydantic Schemas and the BaseTool Pattern
The book's CrewAI CurrencyConverterTool example shows the standard shape every custom tool follows: a Pydantic input schema defining exactly what arguments the tool needs, plus a BaseTool subclass that wires that schema to the actual logic.
~15 min
Tool Selection: How the LLM Decides When and Which Tool to Call
The LLM picks a tool by reading its name and description against the current task — which means prompt/description design directly determines whether the right tool gets called at the right time.
~15 min
Tool Execution: The _run Method, Error Handling, and Permission Prompts
Every custom tool needs a _run method executed when the agent wants to use it — the book's example fetches live exchange rates and explicitly handles failed requests and invalid currency codes rather than letting them crash the agent.
~15 min
Tool Chaining: Multiple Tools in Sequence, Passing Outputs Between Tools
Real tasks often need more than one tool call in sequence, with one tool's output feeding the next tool's input — a natural extension of the book's single-tool CurrencyConverterTool example to multi-step, multi-tool workflows.
~15 min