MCP server tools for an agent
An MCP server is a separate program that offers tools over the Model Context Protocol, and an agent given mcps=[...] gets those tools without holding the code itself.
Last updated: 28 Sep, 2026 · CrewAI 1.15
The tools lesson kept the lookup function in the same file as the crew. In a real shop the order system belongs to another team, which can publish it as an MCP server. Any MCP client can use it, CrewAI included, so the desk calls the order system without importing it.
A server that publishes one tool
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders")
ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}
@mcp.tool()
def lookup_order(order_id: str) -> str:
"""Look up an order's shipping status by its id, such as A17."""
status = ORDERS.get(order_id)
return f"{order_id} {status}." if status else f"{order_id} is not an order we have."
if __name__ == "__main__":
mcp.run()This uses FastMCP from the mcp package, which CrewAI installs. The order data lives here, in the server, not in the crew. mcp.run() talks over standard input and output, so a client starts it as a subprocess.
Pointing the agent at the server
from crewai import Agent
from crewai.mcp import MCPServerStdio
import sys
clerk = Agent(role="Order clerk", goal="Find the status of customers' orders",
backstory="You can look up any order.", llm=ShopLLM(model="shop"),
mcps=[MCPServerStdio(command=sys.executable, args=["orders_server.py"])])MCPServerStdio says how to start the server: the command and its arguments. sys.executable is the Python now running, which is the one that has mcp installed. The agent gets no tools=; mcps= brings them.
View the code here
import json
import os
import re
from crewai import BaseLLM
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["CREWAI_TRACING_ENABLED"] = "false"
os.environ["CREWAI_DISABLE_VERSION_CHECK"] = "true"
class ShopLLM(BaseLLM):
script: list = []
def supports_function_calling(self):
return True
def call(self, messages, tools=None, **kwargs):
if isinstance(messages, str):
messages = [{"role": "user", "content": messages}]
if self.script:
return self.script.pop(0)
return self.decide(messages, tools or [])
def decide(self, messages, tools):
last = messages[-1]
if last["role"] == "tool":
return last["content"]
text = last["content"]
orders = re.findall(r"\b[A-Z]\d+\b", text)
want_refund = "refund" in text.lower()
chosen = None
for t in tools:
fn = t["function"]
label = (fn["name"] + " " + (fn.get("description") or "")).lower()
is_refund = "refund" in label
if want_refund and is_refund:
chosen = fn["name"]
break
if not want_refund and not is_refund and ("look up" in label or "status" in label):
chosen = fn["name"]
break
if orders and chosen:
args = json.dumps({"order_id": orders[0]})
return [{"id": f"call_{orders[0]}", "type": "function",
"function": {"name": chosen, "arguments": args}}]
if "working with:" in text:
context = text.split("working with:")[1].strip().split("\n\n")[0]
return f"Dear customer, {context}"
if orders:
return f"I have no way to look up {orders[0]} yet."
return "Hello. Which order is this about?"
Two orders answered by the server
import os, sys
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
from crewai import Agent, Crew, Task
from crewai.mcp import MCPServerStdio
from shop_llm import ShopLLM
def desk():
clerk = Agent(role="Order clerk", goal="Find the status of customers' orders",
backstory="You can look up any order.", llm=ShopLLM(model="shop"),
mcps=[MCPServerStdio(command=sys.executable, args=["orders_server.py"])],
verbose=False)
task = Task(description="Answer the customer: {question}",
expected_output="The order's status in one sentence.", agent=clerk)
return Crew(agents=[clerk], tasks=[task])
for q in ["Where is my order C40?", "Where is my order B22?"]:
print(desk().kickoff(inputs={"question": q}).raw)C40 waiting for stock. B22 is not an order we have.
Reading where the answers came from
- CrewAI started the server, asked it for its tools, and turned each into a tool the agent can call.
- C40 and B22 were answered from the server's data, not the stand-in: the crew has no order table, so both lines could only come from the running server.
- B22 came back as not found because the server returned that, and the desk passed it on instead of inventing a status.
- The tool's name is prefixed with the command and arguments that start the server, then shortened to fit a provider's 64-character limit, so it is not the bare
lookup_order.
A local server against a remote one
MCPServerStdio runs the server as a subprocess on this machine. MCPServerHTTP connects to one already running elsewhere; the agent code is the same.
| MCPServerStdio | MCPServerHTTP | |
|---|---|---|
| Where it runs | A subprocess you start | A service running elsewhere |
| You give it | A command and arguments | A URL and headers |
| Fits | A local tool or a dev run | A shared, deployed tool |
When to reach for MCP
- The tool belongs to another team and you should not copy its code.
- The same tool is shared by many agents or many apps.
- You want to swap a local tool for a deployed one without touching the agent.
command must be a Python that has mcp installed, which is why sys.executable is safer than a bare "python": a plain name can point at a different interpreter with no mcp, and the agent then starts with no tools and says it cannot look anything up.Related
- Previous: Memory: remember and recall across runs
- Next: Real model: swapping in a hosted LLM
- Reference: CrewAI docs, MCP overview
- Add a second tool to the server, such as
order_total, and ask for it. - Change
argsto a file that does not exist and read what the agent does. - Swap
MCPServerStdioforMCPServerHTTPpointing at a URL and read the change.
Slow is fine. Stopping is the only problem.