Why this matters
Every AI team that builds more than one agent faces the same problem: you write a tool integration for one project, then rewrite it for the next because there is no standard way to package and share tool implementations. Direct function calling is tightly coupled to the app codebase. LangChain tools only work inside LangChain. OpenAI function schemas differ slightly from Anthropic tool schemas.
Model Context Protocol (MCP) solves this at the architectural level. It defines a single, open standard for how a client (your app, Claude Desktop, any agent) discovers and calls tools provided by a server (a separate process you control). Once you understand MCP, you can build a tool server once and reuse it across every AI product your team ships — and use the growing ecosystem of pre-built MCP servers without writing integration code.
For application engineers, MCP is now a first-class interview topic. Teams building production agent infrastructure ask candidates to demonstrate they understand when MCP is the right choice versus simpler alternatives.
Core concepts
MCP vs direct function calling vs LangChain tools
Direct function calling is the lowest abstraction: you define a JSON Schema for each function, pass the schema array to the provider API, and handle tool execution in your application code. It works for single-provider, single-app scenarios but is tightly coupled — the tool definition lives in your app, and every new consumer must copy it.
LangChain tools wrap functions in a class hierarchy. They compose well within LangChain's chain system, support callbacks, and have a large ecosystem of pre-built tools. But they are LangChain-specific. If you switch providers, drop LangChain, or want to share tools with a non-LangChain consumer, you rewrite.
MCP is a network protocol. An MCP server is a separate process (or remote service) that exposes capabilities through a standardized JSON-RPC interface. The client discovers available tools at runtime, converts them to the provider's native format, and calls the server to execute them. The server and client are fully decoupled — the server does not know which client is calling, and the client does not know how the server implements the tool.
| Direct function calling | LangChain tools | MCP | |
|---|---|---|---|
| Portability | App-specific | LangChain-specific | Any MCP client |
| Discovery | Static (hardcoded) | Static | Dynamic at runtime |
| Deployment | In-process | In-process | Out-of-process |
| Reusability | None | Within LangChain | Across all clients |
| Setup complexity | Low | Medium | Medium-high |
Use MCP when you are building tools that multiple agents or products will share, when you want to use pre-built servers from the ecosystem, or when you need out-of-process isolation (tools that access filesystems or databases should run with minimal privileges in their own process).
MCP architecture
MCP defines four layers:
Primitives: What the server exposes.
- Tools — callable functions the model can invoke (equivalent to function calling)
- Resources — read-only data the model can request (files, database rows, API responses)
- Prompts — reusable prompt templates with arguments the client populates
Hosts: The application that embeds the LLM (Claude Desktop, your custom app, an IDE plugin). The host manages the conversation and decides when to forward tool calls to an MCP server.
Clients: The MCP protocol layer inside the host. Clients maintain a session with each MCP server, handle discovery, and translate between MCP wire format and provider-specific tool schemas.
Transports: How client and server communicate.
- stdio — the host spawns the server as a subprocess; client writes to stdin, reads from stdout. Simple and secure for local tools.
- HTTP + SSE — the server runs as a web service; client sends JSON-RPC over HTTP, receives events over Server-Sent Events. Required for remote tools or multi-user deployments.
Building an MCP server in Python
# server.py
import asyncio
import json
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent, CallToolResult
from typing import Any
app = Server("file-search-server")
@app.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="search_files",
description=(
"Search for files in a directory by name pattern. "
"Use when the user asks to find files, locate code, "
"or check if a file exists."
),
inputSchema={
"type": "object",
"properties": {
"directory": {
"type": "string",
"description": "Absolute path to search in"
},
"pattern": {
"type": "string",
"description": "Filename glob pattern, e.g. '*.py'"
},
"max_results": {
"type": "integer",
"default": 20,
"description": "Maximum number of results to return"
}
},
"required": ["directory", "pattern"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
if name != "search_files":
return [TextContent(type="text", text=f"Unknown tool: {name}")]
import glob
import os
directory = arguments["directory"]
pattern = arguments["pattern"]
max_results = arguments.get("max_results", 20)
if not os.path.isdir(directory):
return [TextContent(type="text", text=f"Directory not found: {directory}")]
matches = glob.glob(os.path.join(directory, "**", pattern), recursive=True)
matches = matches[:max_results]
if not matches:
return [TextContent(type="text", text="No files matched the pattern.")]
result = f"Found {len(matches)} file(s):\n" + "\n".join(matches)
return [TextContent(type="text", text=result)]
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
InitializationOptions(
server_name="file-search-server",
server_version="0.1.0",
),
)
if __name__ == "__main__":
asyncio.run(main())
Building an MCP client that discovers and calls tools
# client.py
import asyncio
import anthropic
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def run_agent(user_message: str) -> str:
server_params = StdioServerParameters(
command="python",
args=["server.py"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Dynamic tool discovery — no hardcoded schemas
tools_result = await session.list_tools()
# Convert MCP tool schemas to Anthropic format
anthropic_tools = [
{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
}
for tool in tools_result.tools
]
client = anthropic.Anthropic()
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=anthropic_tools,
messages=messages,
)
if response.stop_reason == "end_turn":
text_blocks = [b for b in response.content if hasattr(b, "text")]
return text_blocks[-1].text if text_blocks else ""
# Append assistant turn
messages.append({"role": "assistant", "content": response.content})
# Execute tool calls via MCP server
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = await session.call_tool(block.name, arguments=block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result.content[0].text if result.content else "",
})
messages.append({"role": "user", "content": tool_results})
if __name__ == "__main__":
answer = asyncio.run(run_agent("Find all Python files in /home/user/project"))
print(answer)
When to use MCP vs rolling your own tool integration
Use MCP when:
- Multiple agents or products need the same tool — build once, share everywhere
- You want to use pre-built servers (databases, filesystems, APIs) from the ecosystem
- Tools need out-of-process isolation (security boundary between agent and tool execution)
- You are building a platform where teams will contribute their own tool servers
Roll your own when:
- You have a single-provider, single-app tool with no reuse requirement
- The added process overhead and stdio/HTTP complexity is not worth it for the use case
- Your team is not yet familiar with MCP and you need to ship fast
- Tools are purely in-memory transformations with no external I/O
The key signal: if you are writing the same tool integration code for the third time, it is time to build an MCP server.
Common mistakes
Registering too many tools on a single server. Every tool schema consumes context window tokens. A server with 50 tools sends 5,000+ tokens of schema before the user's message. Scope servers to a single domain (one server for database tools, one for file tools) and use tool filtering if discovery is dynamic.
Skipping input validation in the server. The MCP client passes tool arguments directly from the model's output. Models hallucinate parameter values. Always validate inputs server-side using Pydantic or explicit checks — JSON Schema in the tool definition tells the model what to send but cannot prevent it from sending garbage.
Using HTTP transport without authentication. Stdio transport runs locally and is sandboxed by OS process isolation. HTTP transport is exposed to the network. Add API key authentication or mTLS before deploying any HTTP MCP server that accesses sensitive data.
Treating MCP as a replacement for all tool patterns. MCP adds process and protocol overhead. For simple, single-use tools in a single application, direct function calling is faster to build and easier to debug. Adopt MCP for the reusability and isolation benefits, not because it is the newer pattern.
Try it yourself
Build a minimal MCP server that exposes two tools: read_file(path: str) -> str and write_file(path: str, content: str) -> str. Add input validation that (1) rejects paths outside a designated sandbox directory, (2) rejects writes larger than 10KB. Then write a client that connects to this server and runs a small agent loop: given the instruction "summarize the contents of README.md and save the summary to summary.txt", the agent should read the file, generate a summary using the LLM, then write it back using the tool. Verify that both the read and write paths hit the server correctly by adding a log line to each tool handler.