CrewAICrewAI 1.15 · Python 3.10 to 3.13
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
35 small wins to finish your pathNext lesson →

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

python
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

python
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.

Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports this file. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep it in the same folder.
View the code here
shop_llm.py
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

Examplecrew.py
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)

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.

MCPServerStdioMCPServerHTTP
Where it runsA subprocess you startA service running elsewhere
You give itA command and argumentsA URL and headers
FitsA local tool or a dev runA 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.
Watch out
The 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.
Try it yourself
  • Add a second tool to the server, such as order_total, and ask for it.
  • Change args to a file that does not exist and read what the agent does.
  • Swap MCPServerStdio for MCPServerHTTP pointing at a URL and read the change.

Slow is fine. Stopping is the only problem.