DEV Community

Cover image for MCP Explained, the Protocol That Stopped Me From Writing the Same Integration Twenty Times
Syed Muhammad Ali Raza
Syed Muhammad Ali Raza

Posted on

MCP Explained, the Protocol That Stopped Me From Writing the Same Integration Twenty Times

MCP Explained, the Protocol That Stopped Me From Writing the Same Integration Twenty Times

Written by Syed Muhammad Ali Raza

Ten articles into this series, and every single agent we built could only ever use tools I personally wrote by hand and wired up myself, one at a time, specifically for that one project. Want the same agent to also check GitHub issues in a different project. Write another custom integration. Want it to read from Notion too. Another custom integration. Every new tool meant more glue code, and none of that glue code was reusable anywhere else.

This got old fast, and it turns out I wasn't the only one annoyed by it. MCP, Model Context Protocol, is Anthropic's answer to exactly this problem, and once it actually clicked for me, it genuinely changed how I think about building anything that connects an AI system to the outside world. This article is that explanation, plus real code, plus a couple of things to actually make you laugh along the way since integration hell deserves at least a little humor.

A real life example before any of the protocol stuff

Think about traveling internationally before universal travel adapters existed. Every single country had its own plug shape, its own voltage standard. If you were a device manufacturer trying to sell a charger that worked everywhere, you'd need to build a completely different physical connector for every single country you shipped to. A charger built for UK outlets was useless in Japan. One built for Japan didn't fit in Germany. Every manufacturer, for every device, solving the exact same problem over and over, country by country, device by device.

Then universal adapters showed up, and more importantly, USB became a genuinely standard connector that basically everything agreed to support. Suddenly a phone charger built once works in any country, plugged into any wall, through one small adapter, instead of the manufacturer needing to build and support a different physical plug for every single market.

Before MCP, connecting an AI agent to a tool was exactly the country specific charger problem. Want your agent to talk to GitHub, you write custom code specifically for GitHub's API. Want it to also talk to Slack, write completely different custom code specifically for Slack's API. Every AI application that wanted to use GitHub had to write its own GitHub integration, and every AI application that wanted to use Slack had to write its own Slack integration, completely separately, over and over, everywhere, by everyone, all solving the exact same wiring problem independently.

MCP is the USB standard for this exact problem. A tool builder implements MCP once, and suddenly any MCP compatible AI application can use that tool immediately, no custom wiring required on either side.

The actual technical problem, in developer terms, the N times M nightmare

Let me put a number on why this actually matters at scale, because "convenient" undersells it.

Say there are M different AI applications, Claude Desktop, some custom agent you built, a coding assistant, whatever. And there are N different tools people want to connect to those applications, GitHub, Slack, a database, a file system, Notion, Google Drive, on and on. Without a shared standard, every single AI application needs its own custom integration code for every single tool it wants to support. That's M times N separate pieces of integration code, written and maintained by everyone, forever, duplicated endlessly across the entire ecosystem.

With a shared protocol, a tool builder writes one MCP server for their tool, once. An AI application builder writes one MCP client implementation, once. That turns the problem from M times N into just M plus N, and critically, nobody has to coordinate directly with anybody else to make a new pairing work. A brand new AI application that speaks MCP can immediately use every existing MCP server that's ever been built, with zero new integration code, and a brand new tool that speaks MCP is immediately usable by every existing MCP client, same deal. That's genuinely the entire value proposition, in one math fact.

Okay so what actually is MCP, mechanically

At its core, MCP is a client server protocol. An MCP server exposes capabilities, tools it can call, data it can provide, prompts it can offer. An MCP client, which lives inside an AI application, connects to one or more of these servers and makes those capabilities available to the model, using the exact same tool use mechanism we already covered back in the agents article in this series.

The genuinely important part, this is just a standard, agreed upon shape for that connection, using JSON RPC messages under the hood, so any client that speaks this standard can talk to any server that speaks this standard, regardless of who built which side, or what language they wrote it in.

An MCP server typically exposes three kinds of things. Tools, actions the model can actually call, like searching a database or creating a ticket, exactly like the tool use pattern from the agents article. Resources, data the model can read, like the contents of a specific file or a database record. And prompts, pre-written prompt templates the server author provides, so common tasks for that specific tool don't need to be reinvented by every single client that connects to it.

Let's actually build one

I'll build a small MCP server for something genuinely useful, searching and reading local markdown notes, the kind of thing you'd want any AI assistant on your machine to be able to use, without writing custom code into every single assistant separately.

Step 1, install the MCP SDK

pip install mcp
Enter fullscreen mode Exit fullscreen mode

Step 2, build the actual server

from mcp.server.fastmcp import FastMCP
import os
import glob

# this name is what shows up when a client connects to your server
mcp = FastMCP("notes-server")

NOTES_DIRECTORY = os.path.expanduser("~/notes")


@mcp.tool()
def search_notes(query: str) -> str:
    """Search through all markdown notes for a specific keyword or phrase."""
    matches = []
    note_files = glob.glob(os.path.join(NOTES_DIRECTORY, "*.md"))

    for filepath in note_files:
        with open(filepath, "r") as f:
            content = f.read()
            if query.lower() in content.lower():
                filename = os.path.basename(filepath)
                matches.append(filename)

    if not matches:
        return f"No notes found containing '{query}'"

    return f"Found {len(matches)} matching notes: {', '.join(matches)}"


@mcp.tool()
def read_note(filename: str) -> str:
    """Read the full content of a specific note by filename."""
    filepath = os.path.join(NOTES_DIRECTORY, filename)

    if not os.path.exists(filepath):
        return f"Note '{filename}' not found"

    with open(filepath, "r") as f:
        return f.read()


@mcp.tool()
def list_all_notes() -> str:
    """List every markdown note available."""
    note_files = glob.glob(os.path.join(NOTES_DIRECTORY, "*.md"))
    filenames = [os.path.basename(f) for f in note_files]
    return f"Notes available: {', '.join(filenames)}"


if __name__ == "__main__":
    mcp.run()
Enter fullscreen mode Exit fullscreen mode

That's genuinely the entire server. Notice how little of this is actually about MCP itself, most of it is just plain Python doing plain filesystem work. The @mcp.tool() decorator is doing the real heavy lifting, it automatically handles describing this function to any connecting client, exactly like the manually written tool schemas we built by hand back in the agents article, except now you write it once here and never again for every single AI application that wants to use it.

Step 3, connecting a client to this server

Here's the part that actually shows the payoff. Any MCP compatible client, Claude Desktop, a custom agent you build, some other developer's tool entirely, can connect to this exact server without you writing a single line of client specific integration code.

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import asyncio

async def use_notes_server():
    server_params = StdioServerParameters(
        command="python",
        args=["notes_server.py"]
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # ask the server what tools it actually offers
            tools_response = await session.list_tools()
            print("Available tools:")
            for tool in tools_response.tools:
                print(f"  - {tool.name}: {tool.description}")

            # actually call one of them
            result = await session.call_tool("search_notes", {"query": "project ideas"})
            print(f"\nSearch result: {result.content}")

asyncio.run(use_notes_server())
Enter fullscreen mode Exit fullscreen mode

Notice that this client code has zero knowledge of how search_notes is actually implemented, no filesystem logic, no glob patterns, nothing. It just asks the server what's available and calls what it needs. That separation is genuinely the entire point, the client and server can be built by completely different people, updated independently, and neither side needs to know the other's internal implementation details at all.

Wiring this into an actual agent

Here's where it connects directly back to the agent loop we built in an earlier article in this series. Instead of manually defining tool schemas and writing execution functions by hand like we did back then, an MCP aware agent just asks each connected server what it offers and treats those as available tools automatically.

async def run_agent_with_mcp(user_question, session):
    tools_response = await session.list_tools()

    # convert MCP tool descriptions into the same format our
    # agent loop from the agents article already expects
    mcp_tools = [
        {
            "name": tool.name,
            "description": tool.description,
            "input_schema": tool.inputSchema
        }
        for tool in tools_response.tools
    ]

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=mcp_tools,
        messages=[{"role": "user", "content": user_question}]
    )

    if response.stop_reason == "tool_use":
        for block in response.content:
            if block.type == "tool_use":
                # instead of calling a local python function directly,
                # we call it through the MCP session, same idea,
                # just routed through the protocol
                result = await session.call_tool(block.name, block.input)
                print(f"Tool result: {result.content}")

    return response
Enter fullscreen mode Exit fullscreen mode

The agent loop itself barely changed from what we built earlier in the series, the only real difference is where the tool definitions and execution actually come from, hardcoded locally before, dynamically discovered through MCP now. That's genuinely the upgrade, same mental model, dramatically less one-off wiring.

The developer pain this actually saves you from

I want to be specific about what this actually saves in practice, because "standardization" sounds nice in the abstract but the real payoff is very concrete. Before something like MCP existed, adding a new tool to an agent meant reading that tool's specific API docs, writing authentication handling specific to that service, writing request and response parsing specific to that service, and doing this completely from scratch for every single tool, every single time, in every single project that wanted to use it.

With MCP, once a server exists for a tool, connecting to it is the same few lines of client code regardless of what the tool actually does underneath. The GitHub server and the Slack server and your own custom notes server all get connected to and called the exact same way from the client's perspective, even though what's happening inside each server is completely different.

A few honest limitations worth knowing

MCP is still a genuinely young standard, and it's not magic, worth being clear eyed about a few things.

Trust still matters enormously, connecting to an MCP server means giving it the ability to expose tools your agent might call, and everything from the security article earlier in this series about least privilege, human confirmation for sensitive actions, and logging still fully applies here. MCP standardizes the wiring, it doesn't automatically make every server trustworthy or every tool call safe to run without oversight.

Not every tool has an MCP server built for it yet, since it's a genuinely newer standard, so you'll still write custom integrations sometimes, just hopefully fewer over time as more of the ecosystem adopts it.

And discovery is still evolving, right now you generally need to know which MCP servers exist and configure your client to connect to them directly, there isn't yet some universal automatic marketplace where every agent instantly finds and safely vets every available server on its own. That tooling is actively being built out across the ecosystem as we speak.

Bringing this back to the whole series

This connects directly to nearly everything earlier in this series. The tool use mechanism from the agents article is exactly what MCP servers ultimately plug into. The security practices from that article don't go away just because the wiring got standardized, they matter just as much, maybe more, once connecting a new tool becomes this easy and low friction. And the multi-agent patterns from a few articles back become genuinely more practical once different specialist agents can all pull from the same shared pool of MCP servers instead of each one needing its own hand rolled integration code.

The actual shift MCP represents is a simple one once you see it, tool integration stops being a one-off thing you build fresh for every single project, and starts being infrastructure you build once and reuse everywhere, exactly the same shift that happened when USB replaced a drawer full of incompatible country specific chargers.


If you build your own MCP server for something you personally use daily, I'd genuinely like to hear what it does, that's usually where the actually useful ones come from, solving your own annoying, specific problem first.

Top comments (0)