A practical guide to how MCP servers work, how to build one in Python, and when to use the protocol to connect AI agents to real developer tools.
Every time a developer builds a new AI integration, they solve the same problem from scratch. How does the LLM call a function? How does it read a file? How does it query a database? How do you pass the results back in a format the model understands? The boilerplate piles up fast, and none of it transfers to the next project.
Model Context Protocol — MCP — is Anthropic's answer to this. It is an open standard, launched in November 2024, that defines a clean interface between AI hosts (like Claude Desktop or any agent runtime) and the external tools and data sources agents need to do real work. Think of it as USB for AI agents: one standard connector that works across tools, runtimes, and use cases.
This is worth understanding even if you are not building agents full-time. MCP is fast becoming the default integration layer for serious AI-assisted developer workflows. If you are building with Claude, Cursor, or any tool-calling LLM in 2025, you will encounter it. And if you are building custom agentic tooling, knowing how to implement an MCP server correctly will save you weeks.
The problem MCP solves is real. Before it, every tool-calling implementation was bespoke. OpenAI's function calling API, Anthropic's own tool use, LangChain's tools abstraction — they all solve the same problem with different schemas, lifecycle contracts, and error semantics. Building a "read file" tool for one LLM runtime meant you could not reuse it in another.
MCP replaces that fragmentation with a single protocol. An MCP server implements one interface. Any MCP-compatible host — Claude Desktop, a custom agent, an IDE plugin — can connect to that server and use its tools without any adapter code.
The practical result: there is now a growing library of pre-built MCP servers covering GitHub, Slack, filesystem access, PostgreSQL, SQLite, web search, and more. You can wire any of these into your agent workflow without writing a line of integration code.
The architecture is straightforward. There are three participants:
An MCP host is the thing running the AI model — Claude Desktop, a custom Python script, Cursor. It manages connections to one or more servers.
An MCP client lives inside the host and handles the protocol communication with a single server.
An MCP server is a standalone process that exposes capabilities — tools, resources, or prompt templates — over a defined interface.
Communication happens over JSON-RPC 2.0. Two transport mechanisms are supported: stdio (the host spawns the server as a subprocess and communicates over stdin/stdout) and HTTP with Server-Sent Events (for remote servers accessible over a network).
Stdio is by far the most common pattern for local developer tooling. You write a small Python or TypeScript program, register it in your Claude Desktop config file, and the host starts it automatically when needed. No ports, no auth tokens, no deployment required.
MCP servers expose three types of primitives:
Tools are functions the model can call — like read_file, run_query, create_github_issue. The server declares what each tool does, what parameters it accepts, and what it returns. The model decides when to call them.
Resources are data the model can read — files, database records, API responses. Unlike tools, resources are passive: the model reads them rather than triggering an action.
Prompts are reusable prompt templates. Less commonly used, but useful when you want to standardize how an agent reasons about a particular type of task.
Here is a working example of a simple MCP server in Python using the official SDK. This server exposes a single tool that reads a JSON file from a specified directory.
import json
from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("file-reader")
@mcp.tool()
def read_json_file(filename: str) -> dict:
"""Read a JSON file from the project data directory."""
safe_path = Path("/data/project") / Path(filename).name # no path traversal
if not safe_path.exists():
raise FileNotFoundError(f"File not found: {filename}")
with open(safe_path) as f:
return json.load(f)
if __name__ == "__main__":
mcp.run()
Wire it into Claude Desktop by adding to claude_desktop_config.json:
{
"mcpServers": {
"file-reader": {
"command": "python",
"args": ["/path/to/your/server.py"]
}
}
}
Restart Claude Desktop. The tool is now available in conversations. When Claude decides it needs to read a JSON file, it calls your server — no prompt engineering required to explain the API.
The FastMCP wrapper handles all the protocol boilerplate. The actual server code is just the function. The TypeScript SDK works similarly using the @mcp/server package.
MCP is worth using when:
Skip it (or defer it) when:
The protocol is lightweight, but it does add process management overhead. For a quick experiment, the extra structure is friction. For anything production-grade or team-facing, the standardization pays off.
Ignoring path traversal in file tools. The example above sanitizes the input path for a reason. If your tool accepts a filename and opens it naively, an agent can be prompted to read files outside your intended scope. Always resolve to a known base directory and reject paths that escape it.
Returning too much data in tool responses. MCP tool responses go straight into the model's context window. If your database query returns 10,000 rows, you have just consumed most of your context budget with raw data. Paginate, summarize, or filter at the tool layer before returning.
Not handling errors gracefully. If a tool throws an unhandled exception, the MCP host behavior is implementation-specific. Some hosts surface the error to the model, some silently fail. Return structured error responses explicitly so the model knows what went wrong and can decide whether to retry or report.
Building tools that are too granular. A tool that does one tiny thing forces the model to chain many calls to accomplish a task, each one adding latency and potential failure points. Design tools around developer intent, not around API endpoints. get_pull_request_with_files is more useful than separate get_pull_request and list_pull_request_files calls.
Running MCP servers with excessive permissions. If your server can write to disk, create files, or execute commands, those capabilities are now accessible to any model that connects to it. Scope permissions to exactly what the tools need. Do not run the server with admin privileges because it is convenient.
Start with the official SDK. FastMCP in Python and the TypeScript SDK handle the JSON-RPC layer, connection lifecycle, and transport negotiation for you. Writing raw protocol handling is unnecessary and error-prone.
Document your tools clearly. MCP tool descriptions are read by the model at connection time to understand what each tool does. Write them like you would write a good docstring: what the tool does, when to use it, what the parameters mean, what the return value looks like. The model's decision to use a tool well depends entirely on this.
Test with Claude Desktop before building your own host. The desktop client gives you interactive feedback on tool behavior quickly. You can see what the model chooses to call, what it passes, and what it does with the response. This is faster than instrumenting a custom agent loop.
Version your tools carefully. If you change a tool's parameter names or return shape, existing agents that depend on it may break silently. Treat tool interfaces like API contracts.
Log everything. Tool calls in an agent workflow are often the hardest part to debug. Log tool name, input parameters, output, and any errors at the server level. When something goes wrong, you want a clear record of exactly what the model tried to do.
If you are building any AI tooling that will be used more than once: implement it as an MCP server. The overhead of the initial setup is small, and you get immediate compatibility with Claude Desktop, Cursor, and any other MCP-compatible client. The pre-built server ecosystem is also worth checking before you write anything — GitHub, Slack, database, and web search servers already exist and are production-quality.
If you are evaluating whether to migrate existing tool-calling code to MCP: worth it if the tools are used across multiple runtimes or by multiple people. Not worth it if the tooling is tightly coupled to application state you cannot easily pass over a process boundary.
The one place to be genuinely cautious is security. MCP servers run locally with whatever permissions the host process has. Any tool that writes to disk, spawns processes, or calls external APIs is a potential blast radius. Treat every input as untrusted. Scope access to the minimum necessary surface. Add confirmation steps before irreversible actions.
MCP is not a magic layer that makes AI agents reliable. The model will still call tools at the wrong time, pass unexpected inputs, and produce wrong outputs. What MCP does is remove the integration friction — one standard interface, reusable servers, no adapter boilerplate.
That is genuinely useful. The developer effort that previously went into wiring tools to LLMs now goes into building better tools. The agents built on top are more composable, more maintainable, and easier to debug.
The ecosystem is early but moving fast. If you are building any kind of AI-assisted developer workflow, getting comfortable with MCP now is a good investment.
Comments
Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.