AI Agents and MCP in Production · The Model Context Protocol (MCP)
MCP: One Protocol Instead of N×M Integrations
Why every AI app re-implemented the same integrations, and how MCP's servers, clients and transports replace them.
Scenario
Example company: three internal AI apps (a support assistant, a coding assistant and an analytics chatbot) all need the same systems: the ticket tracker, the wiki and the customer database. Each team wrote its own integration code.
What you see in production
The ticket tracker changes its API. Three teams fix three integrations over two weeks, each with slightly different bugs. A fourth team wants to add the wiki to its new agent and starts writing integration number four. Auth, pagination and error handling are implemented differently everywhere.
The naive code
# support_assistant/tools.py, coding_assistant/tools.py, analytics_bot/tools.py: three copies
def search_tickets(query):
r = requests.get(f"{TRACKER}/api/v3/search", params={"q": query}, headers={"Authorization": TOKEN})
return [t["title"] for t in r.json()["items"]] # each copy parses, pages and fails differently
Reproduce it
A minimal in-process MCP-style exchange: one server, any client, JSON-RPC messages in between:
import json
class TicketServer:
tools = [{"name": "search_tickets", "description": "Search tickets by text. Returns id, title, status.",
"inputSchema": {"type": "object", "properties": {"query": {"type": "string"}},
"required": ["query"]}}]
data = [{"id": 101, "title": "Login fails on Safari", "status": "open"},
{"id": 102, "title": "Export to CSV is slow", "status": "closed"}]
def handle(self, msg):
if msg["method"] == "initialize":
result = {"protocolVersion": "2025-11-25", "capabilities": {"tools": {}},
"serverInfo": {"name": "tickets", "version": "1.0"}}
elif msg["method"] == "tools/list":
result = {"tools": self.tools}
elif msg["method"] == "tools/call":
q = msg["params"]["arguments"]["query"].lower()
hits = [t for t in self.data if q in t["title"].lower()]
result = {"content": [{"type": "text", "text": json.dumps(hits)}], "isError": False}
return {"jsonrpc": "2.0", "id": msg["id"], "result": result}
server = TicketServer()
for i, (method, params) in enumerate([("initialize", {"clientInfo": {"name": "support-assistant"}}),
("tools/list", {}),
("tools/call", {"name": "search_tickets", "arguments": {"query": "login"}})], 1):
reply = server.handle({"jsonrpc": "2.0", "id": i, "method": method, "params": params})
print(method, "->", json.dumps(reply["result"])[:110])
Output
initialize -> {"protocolVersion": "2025-11-25", "capabilities": {"tools": {}}, "serverInfo": {"name": "tickets", "version":
tools/list -> {"tools": [{"name": "search_tickets", "description": "Search tickets by text. Returns id, title, status.", "in
tools/call -> {"content": [{"type": "text", "text": "[{\"id\": 101, \"title\": \"Login fails on Safari\", \"status\": \"openRoot cause
Without a shared protocol, integrations are N apps × M systems: every app owns its own copy of every integration. The model side is already standard (tool definitions with a name, description and JSON Schema); what was missing is a standard way for an app to discover and call tools that live elsewhere.
The fix
Wrap each system once in an MCP server. Any MCP-capable app (the host, with one client per server connection) discovers the server's tools at runtime and passes them to its model:
from mcp.server import MCPServer
mcp = MCPServer("Tickets")
@mcp.tool()
def search_tickets(query: str, status: str = "open", limit: int = 20) -> list[dict]:
'''Search tickets by text. Returns id, title and status, newest first.'''
return tracker.search(query, status=status, limit=limit)
if __name__ == "__main__":
mcp.run() # stdio by default; mcp.run(transport="streamable-http") for remote use
Key concepts:
- Primitives: servers offer tools (actions the model can call), resources (data the app can read) and prompts (reusable templates).
- Transports: stdio for local servers started by the host, Streamable HTTP for remote servers.
- Lifecycle:
initializenegotiates protocol version and capabilities, thentools/listandtools/calldo the work.
Trade-offs
- MCP adds a process or network hop; for one app talking to one system, a plain in-process tool is simpler.
- Servers expose a generic interface; app-specific shortcuts may still belong in the app.
- The protocol evolves; pin the protocol version your client and server support and test upgrades.
Detect it in production
- Inventory of integrations per system: duplicates across apps are candidates for one MCP server.
- Per-server call counts, latency and error rates, measured on the server, not in each app.
Key points
- MCP turns N×M integrations into N clients and M servers.
- Servers expose tools, resources and prompts; hosts discover them at runtime.
- stdio for local servers, Streamable HTTP for remote ones.
Further reading: MCP introduction · MCP specification · MCP Python SDK
Practice questions
What problem does MCP solve compared with per-app tool integrations?
Answer
It replaces N apps × M systems custom integrations with one MCP server per system that any MCP-capable app can discover and call, so fixes and auth live in one place.
Name the three server primitives in MCP and what each is for.
Answer
Tools are actions the model can invoke, resources are data the application can read and include as context, and prompts are reusable templates the user or app can select.
When would you use stdio versus Streamable HTTP?
Answer
stdio when the host starts the server locally as a subprocess (developer tools, desktop apps); Streamable HTTP when the server runs remotely and serves many clients over the network.
Open in the app to mark lessons complete, take notes and track your progress.