Most agent frameworks solve tool calling by inventing their own schema. You write a Python decorator, register a function, and the agent can invoke it. This works until you need three different agents to share the same database connector, or you want to swap LangChain for AutoGen without rewriting every tool integration.
Model Context Protocol (MCP) is Anthropic’s answer to this fragmentation. It is a JSON-RPC-based protocol that standardizes how agents discover, negotiate, and invoke tools across different systems. Instead of each framework defining its own tool schema, MCP provides a transport layer that any agent runtime can speak.
What MCP Actually Does
MCP separates tool providers (servers) from tool consumers (clients). A server exposes capabilities like database queries, file system access, or API calls. A client (your agent runtime) connects to one or more servers, discovers available tools, and invokes them through a standard RPC interface.
The protocol handles three core responsibilities:
- Tool discovery: Clients query servers for available tools, their schemas, and required parameters.
- Invocation: Clients send tool requests with arguments; servers execute and return results.
- State management: Servers can maintain session state across multiple tool calls without the client needing to track context.
Unlike OpenAI’s function-calling schema, MCP operates one layer below as the wire protocol that connects agents to tool execution.
Architecture: Server, Client, Transport
MCP uses a client-server model over JSON-RPC 2.0. The transport can be stdio (for local processes), HTTP/SSE (for remote servers), or WebSocket (for bidirectional streaming).
Server responsibilities:
- Expose a list of tools via
tools/list - Validate incoming tool requests
- Execute the tool logic (query a database, read a file, call an API)
- Return structured results or errors
Client responsibilities:
- Connect to one or more MCP servers
- Discover available tools
- Map agent tool calls to MCP requests
- Handle responses and errors
Transport layer:
- JSON-RPC 2.0 for request/response framing
- Stdio for local, sandboxed processes
- HTTP/SSE for stateless remote servers
- WebSocket for long-lived sessions with streaming
Here is a minimal MCP server in Python using the official SDK:
from mcp.server import Server
from mcp.types import Tool, TextContent
app = Server("database-server")
@app.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="query_users",
description="Query user table with SQL",
inputSchema={
"type": "object",
"properties": {
"sql": {"type": "string"}
},
"required": ["sql"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "query_users":
sql = arguments["sql"]
# Use parameterized queries: result = execute_query(sql, params=[])
result = execute_query(sql, params=[])
return [TextContent(type="text", text=str(result))]
The client (your agent runtime) connects, calls tools/list, sees query_users, and invokes it with {"sql": "SELECT * FROM users LIMIT 10"}.
Security Boundaries and Trust Model
MCP does not enforce authentication or authorization. That is your job. The protocol assumes you trust the servers you connect to, or you sandbox them.
Here are the primary attack vectors and recommended mitigations:
| Risk | Mitigation Strategy |
|---|---|
| Malicious server returns crafted payloads | Validate all tool responses against expected schemas (e.g., jsonschema library) |
| Agent sends sensitive data to untrusted server | Whitelist servers by origin, use mTLS for remote connections |
| Server executes arbitrary code via tool arguments | Sanitize inputs, use parameterized queries, run servers in containers |
| Session hijacking in long-lived WebSocket connections | Rotate session tokens, enforce idle timeouts |
If you run an MCP server over HTTP, you need to handle auth yourself. The protocol does not include OAuth, API keys, or RBAC. You can layer those on top, but MCP itself is transport-agnostic.
For local servers (stdio transport), the security model is simpler: the server runs as a subprocess of the client. You control the binary, the environment, and the permissions. This is how Claude Desktop uses MCP for file system access.
State Management and Session Lifecycle
MCP servers can be stateful or stateless. A stateless server treats each tool call as independent. A stateful server maintains context across calls.
Stateless example: A weather API server. Each call to get_forecast(city) is independent.
Stateful example: A database server that opens a connection on first call, reuses it for subsequent queries, and closes it when the client disconnects.
The protocol does not mandate how you manage state. You can:
- Store session data in the server process (memory, SQLite)
- Use a shared cache (Redis) keyed by session ID
- Rely on the transport layer (WebSocket connection = session)
If you use stdio, the session ends when the subprocess exits. If you use WebSocket, the session ends when the connection closes. HTTP/SSE requires you to track sessions via tokens or cookies.
Versioning and Schema Evolution
MCP does not have a built-in versioning system for tool schemas. If you change a tool’s signature, you break clients that depend on the old version.
Strategies for schema changes:
- Additive changes: Add optional parameters. Old clients ignore them.
- Breaking changes: Deploy a new server with a different name (
query_users_v2). Clients opt in. - Deprecation: Mark old tools as deprecated in the description. Clients can choose to migrate.
If multiple agents depend on the same MCP server, you need a deployment strategy. You can:
- Run multiple versions of the server simultaneously (different ports, different names)
- Use feature flags inside the server to toggle behavior
- Version the server endpoint itself (
/v1/tools,/v2/tools)
The protocol does not solve this for you. You need to coordinate schema changes across clients and servers.
Observability and Debugging
MCP is JSON-RPC, so you can log every request and response. The protocol includes error codes and messages, but no built-in tracing or metrics.
What you should instrument:
- Tool invocation latency (client-side and server-side)
- Error rates by tool name
- Argument validation failures
- Transport-level failures (connection drops, timeouts)
If you run multiple MCP servers, you need distributed tracing. Inject trace IDs into JSON-RPC requests and propagate them through your tool execution stack. OpenTelemetry works here.
For local stdio servers, stderr is your friend. Log everything to stderr, and the client can capture it.
MCP lacks built-in retry semantics for transient failures. Clients must implement exponential backoff themselves, and the protocol provides no guidance on distinguishing retryable errors from permanent failures.
When MCP Makes Sense
MCP is useful when you need to share tools across multiple agent runtimes or when you want to decouple tool logic from agent orchestration.
Good fit:
- You run multiple agent frameworks (LangChain, AutoGen, custom) and want a single tool server.
- You need to sandbox tool execution (run servers in containers, enforce resource limits).
- You want to version tools independently of agent code.
Bad fit:
- You have a single agent runtime and no plans to add more. Just use native tool calling.
- Your tools are stateless HTTP APIs. MCP adds overhead without value.
- You need sub-millisecond latency. JSON-RPC over stdio is fast, but not that fast.
Technical Verdict
Use MCP when you need a standard protocol for tool integration across heterogeneous agent systems. It solves the “every framework has its own tool schema” problem and gives you a clean boundary for security, state management, and versioning.
Avoid it if you are building a single-agent system with a fixed set of tools. The abstraction cost is not worth it. Just call your functions directly.
The protocol is production-ready for tool discovery and invocation, but you will need to build your own retry logic, error classification, and session recovery patterns. MCP is the most mature standard for cross-framework tool interoperability today.