CrewAI + MCP: The Same Incident-Control Problem, a Different Architecture
Build the Northstar Cloud multi-agent incident-control system with CrewAI agents and MCP servers, and compare it with the LangGraph/local-tool architecture from Part I.
Part II: CrewAI + MCP
Same Northstar Cloud incident. Same five specialist responsibilities. Different architecture.
Part I: LangGraph explicitly orchestrated Python nodes that called local tools.
Part II: CrewAI organizes role-based agents and tasks, while Model Context Protocol (MCP) places data capabilities behind standardized servers.
The architectural shift is:
from “my application owns every tool function” → to “agents consume capabilities exposed by MCP servers.”
NoteTeaching article + copyable project
This Quarto page does not execute the code while rendering. Every code block is intended to be copied into the named file and run locally.
That keeps your published article independent of API keys, local databases, and running MCP processes.
1. Start from the same business problem
Northstar Cloud receives an incident:
Checkout latency and error rate increased shortly after a deployment.
As in Part I, useful evidence lives in different places:
The small animation above is conceptual: it shows the handoff of responsibility, not background computation in the web page.
4. What CrewAI does and what MCP does
This distinction is the heart of Part II.
CrewAI
CrewAI gives us abstractions for:
agents with roles and goals;
tasks with expected outputs;
task context;
crews;
sequential or other processes;
agent/tool collaboration.
MCP
MCP gives us a standard boundary for:
publishing tools;
discovering tools;
describing tool inputs from Python types;
invoking tools through a client/server protocol;
separating an AI application from the system that owns the data.
The official MCP Python SDK can expose tools, resources, and prompts. For this project we intentionally use tools, because the agents need bounded operations such as get_service_metrics() and search_playbook().
5. What you design versus what libraries handle
You design
business objective;
agent responsibilities;
task boundaries;
prompts and expected outputs;
MCP tool semantics;
permissions;
deterministic escalation rules;
evaluation cases.
Libraries handle
CrewAI agent/task execution;
model invocation;
passing task context;
MCP client/server communication;
tool discovery;
input schemas from type hints;
serialization and protocol details;
starting local stdio MCP subprocesses.
MCP does not decide what your tool should do. CrewAI does not decide what your organization considers safe.
config.json is used by main.py. The MCP data servers do not need the OpenAI key because they serve data/tools; they do not reason with the LLM.
9. Understand an MCP server before building three
A minimal current MCP Python server looks like:
from mcp.server import MCPServermcp = MCPServer("Example")@mcp.tool()def add(a: int, b: int) ->int:"""Add two integers."""return a + bif__name__=="__main__": mcp.run()
For local development, mcp.run() uses stdio by default.
Conceptually:
CrewAI host
│
├── starts server.py as a subprocess
│
├── discovers its tools
│
├── sends a tool request
│
└── receives the result
You did not write JSON-RPC parsing, schema generation, or subprocess messaging. The MCP SDK and host integration handle those mechanics.
10. MCP Server 1 — telemetry
Create servers/telemetry_server.py:
from pathlib import Pathimport jsonimport pandas as pdfrom mcp.server import MCPServerROOT = Path(__file__).resolve().parents[1]CSV_PATH = ROOT /"data"/"telemetry.csv"mcp = MCPServer("northstar-telemetry", instructions="Read-only access to Northstar service telemetry.")def _load() -> pd.DataFrame:return pd.read_csv(CSV_PATH)@mcp.tool()def get_service_metrics(service: str, last_n: int=6) ->str:"""Return the most recent telemetry rows for one service.""" df = _load() rows = ( df[df["service"] == service] .tail(last_n) .to_dict(orient="records") )return json.dumps(rows)@mcp.tool()def summarize_service_metrics(service: str) ->str:"""Return a compact numerical summary for one service.""" df = _load() x = df[df["service"] == service]if x.empty:return json.dumps({"error": "service not found"}) result = {"service": service,"rows": int(len(x)),"latest_p95_latency_ms": float(x.iloc[-1]["p95_latency_ms"]),"latest_error_rate": float(x.iloc[-1]["error_rate"]),"max_p95_latency_ms": float(x["p95_latency_ms"].max()),"max_error_rate": float(x["error_rate"].max()), }return json.dumps(result)if__name__=="__main__": mcp.run()
What is happening?
You wrote ordinary Python functions.
The @mcp.tool() decorator turns them into MCP capabilities. Type hints describe their inputs. The server exposes them; a compatible client discovers and invokes them.
The agent does not receive direct unrestricted access to the CSV.
11. MCP Server 2 — operations database
Create servers/operations_server.py:
from pathlib import Pathimport jsonimport sqlite3from mcp.server import MCPServerROOT = Path(__file__).resolve().parents[1]DB_PATH = ROOT /"data"/"operations.db"mcp = MCPServer("northstar-operations", instructions="Read-only operational metadata and deployment history.")def _query(sql: str, params: tuple) ->list[dict]:with sqlite3.connect(DB_PATH) as conn: conn.row_factory = sqlite3.Row rows = conn.execute(sql, params).fetchall()return [dict(row) for row in rows]@mcp.tool()def get_recent_deployments(service: str, limit: int=5) ->str:"""Return recent deployments for a service.""" rows = _query(""" SELECT service, version, deployed_at, status FROM deployments WHERE service = ? ORDER BY deployed_at DESC LIMIT ? """, (service, limit), )return json.dumps(rows)@mcp.tool()def get_service_owner(service: str) ->str:"""Return ownership and on-call information for a service.""" rows = _query(""" SELECT service, team, on_call FROM services WHERE service = ? """, (service,), )return json.dumps(rows)@mcp.tool()def get_customer_impact(service: str) ->str:"""Return summarized customer-impact records for a service.""" rows = _query(""" SELECT service, impacted_customers, region, observed_at FROM customer_impact WHERE service = ? ORDER BY observed_at DESC LIMIT 5 """, (service,), )return json.dumps(rows)if__name__=="__main__": mcp.run()
Notice what we did not expose:
execute_arbitrary_sql(sql)
That is deliberate. Narrow tools are easier to secure and reason about.
12. MCP Server 3 — incident playbook
Create servers/playbook_server.py:
from pathlib import Pathimport refrom pypdf import PdfReaderfrom mcp.server import MCPServerROOT = Path(__file__).resolve().parents[1]PDF_PATH = ROOT /"data"/"incident_playbook.pdf"mcp = MCPServer("northstar-playbook", instructions="Read-only access to approved incident-response guidance.")def _pages() ->list[tuple[int, str]]: reader = PdfReader(PDF_PATH) result = []for number, page inenumerate(reader.pages, start=1): text = page.extract_text() or"" result.append((number, text))return resultdef _terms(text: str) ->set[str]:return { word.lower()for word in re.findall(r"[A-Za-z]{4,}", text) }@mcp.tool()def search_playbook(query: str, top_k: int=3) ->str:"""Return the most relevant playbook pages using simple keyword overlap.""" query_terms = _terms(query) scored = []for page_number, text in _pages(): score =len(query_terms & _terms(text))if score: scored.append((score, page_number, text)) scored.sort(reverse=True) chunks = []for score, page_number, text in scored[:top_k]: compact =" ".join(text.split()) chunks.append(f"PAGE {page_number} | score={score}\n{compact[:1800]}" )return"\n\n".join(chunks) or"No relevant playbook section found."if__name__=="__main__": mcp.run()
This uses a deliberately simple retrieval method so that MCP remains the focus.
For a larger playbook, replace _terms() and page scoring with an embedding/vector retrieval implementation. The MCP tool contract can remain the same:
That is one of the architectural benefits: implementation behind the server can evolve without forcing every agent application to understand the storage layer.
13. Test each MCP server before involving agents
Do not debug the LLM and MCP simultaneously.
Run the MCP Inspector against each server:
mcp dev servers/telemetry_server.py
Then:
mcp dev servers/operations_server.py
and:
mcp dev servers/playbook_server.py
Check that the expected tools appear and return sensible data.
The development sequence should be:
Data works
↓
MCP server works
↓
MCP tools are discoverable
↓
CrewAI agent uses them
↓
Full crew works
14. Connect CrewAI to MCP
CrewAI supports MCP tools directly. For local Python servers we can use stdio configuration.
This is both an architectural and security decision.
16. Build the CrewAI agents
Continue main.py:
from crewai import Agent, LLMllm = LLM( model=MODEL, temperature=0,)triage_agent = Agent( role="Signal Triage Specialist", goal=("Determine the affected service, severity, and whether ""the incident requires deeper investigation." ), backstory=("You are an SRE triage specialist. You separate observed ""signals from hypotheses and never invent telemetry." ), llm=llm, mcps=[telemetry_mcp], allow_delegation=False, verbose=True,)evidence_agent = Agent( role="Incident Evidence Analyst", goal=("Collect evidence that supports or contradicts possible ""incident explanations." ), backstory=("You investigate telemetry, deployment history, customer ""impact, and approved operational guidance." ), llm=llm, mcps=[telemetry_mcp, operations_mcp, playbook_mcp], allow_delegation=False, verbose=True,)planner_agent = Agent( role="Incident Response Planner", goal=("Recommend the safest next action supported by evidence ""and the incident playbook." ), backstory=("You design operational responses but do not confuse ""correlation with proven root cause." ), llm=llm, mcps=[operations_mcp, playbook_mcp], allow_delegation=False, verbose=True,)critic_agent = Agent( role="Risk and Policy Critic", goal=("Challenge the proposed response for evidence gaps, ""policy violations, and unacceptable operational risk." ), backstory=("You are an independent reviewer. You return ACCEPT, ""REVISE, or ESCALATE with a concise reason." ), llm=llm, mcps=[playbook_mcp], allow_delegation=False, verbose=True,)communication_agent = Agent( role="Stakeholder Communication Specialist", goal=("Produce a concise incident update that communicates only ""reviewed facts and the approved next step." ), backstory=("You communicate clearly without exposing internal secrets ""or turning hypotheses into facts." ), llm=llm, allow_delegation=False, verbose=True,)
The role, goal, backstory, tool access, and permissions are your design.
CrewAI takes care of executing the agent and presenting its available MCP tools.
17. Define the five tasks
An agent is a specialist. A task is the assignment given to that specialist.
from crewai import Tasktriage_task = Task( description=""" Analyze incident {incident_id} for service {service}. Incident report:{description} Use telemetry tools when useful. Report: - observed signals, - affected service, - severity: LOW, MEDIUM, HIGH, or CRITICAL, - whether deeper investigation is needed, - confidence from 0 to 1. Do not diagnose root cause. """, expected_output=("A compact triage report separating observations from hypotheses." ), agent=triage_agent,)evidence_task = Task( description=""" Investigate the incident using available MCP tools. Collect only evidence relevant to: - recent telemetry, - recent deployments, - customer impact, - incident playbook guidance. Clearly label: OBSERVATION HYPOTHESIS CONTRADICTING EVIDENCE MISSING EVIDENCE """, expected_output="A grounded evidence bundle with tool-derived facts.", agent=evidence_agent, context=[triage_task],)planning_task = Task( description=""" Using the triage and evidence reports, propose the safest next action. Consult the operations and playbook tools when needed. Return: ACTION JUSTIFICATION RISK HUMAN APPROVAL REQUIRED: YES/NO UNCERTAINTY Never claim that a recent deployment caused the incident unless the evidence actually establishes causality. """, expected_output="A policy-grounded response plan.", agent=planner_agent, context=[triage_task, evidence_task],)critic_task = Task( description=""" Independently review the response plan. Check: 1. Is the action supported by evidence? 2. Is it consistent with the playbook? 3. Is uncertainty represented honestly? 4. Does the action require human approval? 5. Is important contradicting evidence ignored? End with exactly one verdict: VERDICT: ACCEPT VERDICT: REVISE or VERDICT: ESCALATE """, expected_output="An independent review ending in one explicit verdict.", agent=critic_agent, context=[evidence_task, planning_task],)communication_task = Task( description=""" Prepare the final stakeholder update using the reviewed context. If the critic accepted the plan, communicate the approved next step. If the critic requested revision or escalation, do NOT present the proposed action as approved. State that human review is required. Include: - what is known, - customer impact if known, - current next step, - remaining uncertainty. Keep the message concise. """, expected_output="A concise and safe stakeholder incident update.", agent=communication_agent, context=[triage_task, evidence_task, planning_task, critic_task],)
Notice how context=[...] expresses which earlier task outputs matter to a later task.
CrewAI’s structured MCP configuration also supports tool filtering, which is useful when a server exposes more capabilities than one agent should receive.
24. Failure modes to teach explicitly
MCP server unavailable
The agent may lose a critical evidence source.
Design response: fail safely or escalate rather than inventing evidence.
Tool returns valid but stale data
Protocol success is not business correctness.
Design response: include timestamps and freshness requirements.
Agent chooses the wrong tool
Tool discovery does not guarantee good tool selection.
Design response: improve tool names, docstrings, prompts, and evaluation.
Critic becomes overconfident
A second LLM is not automatically a ground-truth oracle.
Design response: keep deterministic escalation rules and human review.
Too many tools
A large tool menu can increase confusion and risk.
Design response: give each role only the tools it needs.
25. Test the MCP layer separately
Create tests/smoke_test.py to test one server directly with the MCP client:
import anyioimport sysfrom pathlib import Pathfrom mcp import Client, StdioServerParametersROOT = Path(__file__).resolve().parents[1]server = StdioServerParameters( command=sys.executable, args=[str(ROOT /"servers"/"telemetry_server.py")],)asyncdef main():asyncwith Client(server) as client: tools =await client.list_tools() names = [tool.name for tool in tools.tools]print("Tools:", names) result =await client.call_tool("summarize_service_metrics", {"service": "checkout-api"}, )print("Result:", result)if__name__=="__main__": anyio.run(main)
Run:
python tests/smoke_test.py
This test contains no CrewAI agent. That is useful: if it fails, debug MCP/data rather than the LLM.
The final answer alone is not enough. A correct-looking answer reached through the wrong tools or fabricated evidence is still a bad agentic trajectory.
27. Where should different functions live?
As the application grows, separate responsibilities.
Different business cases can replace functions without replacing the whole architecture.
For example:
Incident response Finance Manufacturing
───────────────── ─────── ─────────────
telemetry server → transaction server sensor server
operations server → account server maintenance server
playbook server → policy server quality-manual server
MCP makes those capability boundaries explicit.
28. When would I add CrewAI Flow?
The sequential Crew above is intentionally easy to understand.
If the production requirement becomes:
critic ACCEPT → communication
critic REVISE → planner again
critic ESCALATE → human
then use a more explicit orchestration layer.
CrewAI provides Flows for event-driven state, branching, and more deterministic orchestration. A strong production design can therefore be:
This is conceptually close to what LangGraph gave us in Part I, but remains within the CrewAI ecosystem.
29. Part I and Part II side by side
Show code
flowchart TB subgraph P1["Part I — LangGraph"] L1[Explicit graph] --> L2[Agent nodes] L2 --> L3[Local Python tools] L3 --> L4[CSV / DB / PDF] end subgraph P2["Part II — CrewAI + MCP"] C1[Crew / Tasks] --> C2[Role-based Agents] C2 --> C3[MCP clients] C3 --> C4[MCP Servers] C4 --> C5[CSV / DB / PDF] end
flowchart TB
subgraph P1["Part I — LangGraph"]
L1[Explicit graph] --> L2[Agent nodes]
L2 --> L3[Local Python tools]
L3 --> L4[CSV / DB / PDF]
end
subgraph P2["Part II — CrewAI + MCP"]
C1[Crew / Tasks] --> C2[Role-based Agents]
C2 --> C3[MCP clients]
C3 --> C4[MCP Servers]
C4 --> C5[CSV / DB / PDF]
end
Part I emphasizes:
STATE + NODES + EDGES + ROUTING
Part II emphasizes:
ROLES + TASKS + CREW + STANDARDIZED CAPABILITIES
Neither removes the need for good system design.
30. Which approach is better?
There is no universal winner.
Requirement
I would lean toward
very explicit branching/state machine
LangGraph
complex deterministic retry loops
LangGraph or CrewAI Flow
natural role/task mental model
CrewAI
rapid team-of-agents prototype
CrewAI
reusable tool layer across AI applications
MCP
tools owned by independent teams/services
MCP
small single-process prototype
local tools may be simpler
enterprise capability interoperability
MCP becomes attractive
My verdict for this Northstar example
For teaching orchestration itself, Part I’s LangGraph design is clearer because the reader can see every state transition and conditional edge.
For a modular enterprise version, I prefer the direction of Part II:
\boxed{\text{CrewAI/Flow for agent work} + \text{MCP for capability boundaries}}
Why? The incident system naturally touches observability, operational databases, and policy documents that may be owned by different services. MCP prevents the agent application from becoming the permanent home of every integration.
\text{MCP}
=
\text{standard contract between AI applications and capabilities}.
Put them together:
Business Goal
→
CrewAI Tasks
→
Specialist Agents
→
MCP Tools
→
Enterprise Systems
The framework handles mechanics.
The protocol handles interoperability.
You still design the intelligence, boundaries, permissions, failure behavior, and definition of success.
32. Build checklist
Before calling the project complete:
Once these are true, you have moved beyond “several prompts chained together” and built a real multi-agent architecture with explicit capability boundaries.