All posts
Agentic AI10 min read·

Model Context Protocol (MCP): The Standard That Makes AI Agents Actually Useful

A deep technical guide to Anthropic's Model Context Protocol — how it works, why it matters for AI agents, and how to build your own MCP server from scratch.

MF

Muhammad Farhan

AI Engineer · Founder of Datraxa

In late 2024, Anthropic open-sourced a protocol that quietly changed how AI agents work. Model Context Protocol (MCP) is a standardized way for LLMs to connect to tools, data sources, and services. By mid-2026 it has become the default integration layer for serious agentic systems. If you are building anything with AI agents and you are not using MCP, you are reinventing the wheel.

This post is a complete technical breakdown: what MCP is, how it differs from raw tool calling, and a step-by-step guide to building your own MCP server in Python.

What MCP Actually Is

Before MCP, every AI integration was a bespoke mess. You had function calling in OpenAI format, tool use in Anthropic format, plugins in the old ChatGPT format, and a dozen SDK-specific patterns on top. Each one required custom glue code. MCP replaces all of it with a single open protocol.

The model is: your AI client (Claude, GPT, any LLM) speaks MCP. Your tools (filesystem, database, browser, APIs) expose an MCP server. The client discovers capabilities at runtime, calls them, gets structured results back. No custom adapters, no format mismatches.

ApproachIntegrationSchema definitionPortability
Pre-MCP tool callingCustom per-providerFunction schemas per SDKBreaks on model switch
MCPStandardized open protocolDiscover at runtimeWorks across any MCP-compatible client

The Three Primitives: Tools, Resources, Prompts

MCP exposes three types of capabilities from a server:

  • -Tools - functions the LLM can call. Read a file, query a database, send an HTTP request. Tools take parameters and return results. This is the main building block.
  • -Resources - data the LLM can read. Think of these as context providers: a file, a database row, a URL. Resources are identified by URI and streamed to the model as context.
  • -Prompts - pre-built prompt templates exposed by the server. Useful for standardized workflows like "summarize this document" or "review this code."

In practice, 90% of MCP usage is Tools. Resources and Prompts matter more for complex multi-agent orchestration where context management is explicit.

Building an MCP Server in Python

The official Python SDK makes this straightforward. Here is a minimal MCP server that exposes three tools: read a file, list a directory, and run a shell command.

pip install mcp
# server.py
import asyncio
import subprocess
from pathlib import Path
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp import types

server = Server("filesystem-tools")

@server.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="read_file",
            description="Read the contents of a file",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "Absolute file path"},
                },
                "required": ["path"],
            },
        ),
        types.Tool(
            name="list_directory",
            description="List files and directories at a path",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "Directory path"},
                },
                "required": ["path"],
            },
        ),
        types.Tool(
            name="run_command",
            description="Run a shell command and return stdout",
            inputSchema={
                "type": "object",
                "properties": {
                    "command": {"type": "string", "description": "Shell command to run"},
                },
                "required": ["command"],
            },
        ),
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    if name == "read_file":
        path = Path(arguments["path"])
        if not path.exists():
            return [types.TextContent(type="text", text=f"Error: {path} not found")]
        return [types.TextContent(type="text", text=path.read_text())]

    elif name == "list_directory":
        path = Path(arguments["path"])
        entries = [str(p.name) + ("/" if p.is_dir() else "") for p in sorted(path.iterdir())]
        return [types.TextContent(type="text", text="\n".join(entries))]

    elif name == "run_command":
        result = subprocess.run(
            arguments["command"], shell=True, capture_output=True, text=True, timeout=30
        )
        output = result.stdout or result.stderr or "(no output)"
        return [types.TextContent(type="text", text=output)]

    return [types.TextContent(type="text", text=f"Unknown tool: {name}")]

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="filesystem-tools",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=None, experimental_capabilities={}
                ),
            ),
        )

if __name__ == "__main__":
    asyncio.run(main())

Connecting to Claude Desktop

Once the server is written, you register it in the Claude Desktop config file. Claude then discovers and uses your tools automatically during any conversation.

// claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/)
{
  "mcpServers": {
    "filesystem-tools": {
      "command": "python",
      "args": ["/absolute/path/to/server.py"]
    }
  }
}

Restart Claude Desktop and your tools appear in the tool picker. Claude can now read your local files, list directories, and run shell commands on your behalf without any additional integration code.

MCP Over HTTP: Stateless Servers for Production

The stdio transport used above is for local tools. For production services that need to be called from cloud-hosted agents, MCP supports an HTTP+SSE transport. This is how you expose an MCP server as a web service:

from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route, Mount
import uvicorn

sse = SseServerTransport("/messages/")

async def handle_sse(request):
    async with sse.connect_sse(
        request.scope, request.receive, request._send
    ) as streams:
        await server.run(
            streams[0], streams[1],
            InitializationOptions(
                server_name="my-service",
                server_version="1.0.0",
                capabilities=server.get_capabilities(
                    notification_options=None,
                    experimental_capabilities={},
                ),
            ),
        )

app = Starlette(
    routes=[
        Route("/sse", endpoint=handle_sse),
        Mount("/messages/", app=sse.handle_post_message),
    ]
)

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

This pattern lets you deploy an MCP server on Render, Railway, or any cloud host and connect to it from any MCP client anywhere. The server is stateless per request, scales horizontally, and works with any LLM that speaks MCP.

Real-World MCP Patterns I Use in Production

Database Read Tool

Expose a SQL query tool that lets Claude inspect your production database read-only. Claude can answer questions about your data without you writing custom analysis scripts for every question.

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "query_db":
        conn = await asyncpg.connect(os.getenv("DATABASE_URL"))
        # Force read-only: wrap in a transaction and rollback
        async with conn.transaction():
            rows = await conn.fetch(arguments["sql"])
            await conn.execute("ROLLBACK")
        result = [dict(row) for row in rows]
        return [types.TextContent(type="text", text=json.dumps(result, indent=2))]

Authenticated API Wrapper

Wrap a third-party API with an MCP tool so the LLM can call it without ever seeing your credentials. The server holds the API key; the LLM only receives the response.

Browser Control via Playwright

Expose navigate, click, and extract tools backed by a headless Playwright instance. The LLM can browse the web, fill forms, and extract structured data from any page. This is the pattern behind most serious AI scraping agents in 2026.

Why MCP Won Over Competing Approaches

The alternative to MCP is either raw function calling (which is provider-specific and breaks when you switch models) or custom agent frameworks that define their own tool interfaces (which lock you into that framework). MCP is neither: it is a transport-level protocol that any LLM can implement and any service can expose.

The key design decision that made it win: MCP servers are just processes. They communicate over stdin/stdout or HTTP. You do not need an SDK on the server side if you implement the JSON-RPC protocol directly. This made adoption fast because any existing service could add an MCP interface in a few hours.

MCP in 2026: The Ecosystem

As of mid-2026, the MCP ecosystem includes hundreds of official and community servers: GitHub, Slack, Notion, PostgreSQL, browser control, vector databases, code execution, and more. Claude Desktop, Claude Code, and most serious agent frameworks use MCP as the default tool interface. If you are building a service that AI agents might need to use, publishing an MCP server for it is now standard practice.

Should I use MCP or raw tool calling?

If you are building for a single model and a single deployment context, raw tool calling is simpler. If you are building tools that will be used across different models, different clients, or exposed to third-party agents, use MCP. The protocol overhead is minimal and the portability is worth it.

Does MCP work with models other than Claude?

Yes. MCP is model-agnostic. OpenAI, Google, and open-source frameworks like LangChain and LlamaIndex all have MCP client support in 2026. The server you write today works with GPT-5, Gemini, and any future model that adopts the protocol.

Share this article

Work with me

Need This Built?

I am available for freelance projects, consulting, and remote AI engineering roles. If you need an agentic system, CV pipeline, or scraping infrastructure built properly - let's talk.

Usually responds within 24 hours  ·  Based in Islamabad, open to remote globally