Skip to content
datarekha

MCP — Model Context Protocol

The standard for plugging tools into agents. Write your integration once; every MCP-compatible client can use it.

Written and reviewed by Last reviewed

6 min read Intermediate Agentic AI Lesson 7 of 78

What you'll learn

  • What MCP is and why it exists
  • The architecture — servers, clients, and the three primitives
  • How a minimal MCP server looks in Python
  • Why this matters for the agent ecosystem

Before you start

A platform team ships a “look up an order” tool for their support agent. Then sales wants it in their internal chat assistant. Then an engineer wants it in Cursor. Then someone files a ticket asking why the Claude Desktop integration disagrees with the production version. Four copies of the same forty-line function, all subtly different. By mid-2025 this was every enterprise team’s reality.

For the first two years of the agent boom, every team built tool integrations from scratch. Want your agent to read GitHub? Write a GitHub tool. Want it in Cursor too? Write the same tool again, in Cursor’s plugin format. Slack? Same code, third format.

Each new LLM client meant rebuilding the same connectors.

Model Context Protocol (MCP) is the fix. It’s an open standard, originally proposed by Anthropic and now governed as a vendor-neutral open specification at modelcontextprotocol.io, that defines how an LLM client talks to a tool provider. MCP is not Anthropic-only — any LLM host or agent framework can implement it, and most major ones already do.

The bet is simple: write your integration once as an MCP server, and every MCP-compatible client can use it.

MCP-compatible clients include:

  • Claude Desktop and Claude Code
  • Cursor, Cline, Continue, and VS Code (via the Copilot Chat MCP adapter)
  • Windsurf and Zed
  • ChatGPT (announced support)
  • The major agent frameworks: LangChain, LangGraph, MS Agent Framework, and Google ADK

The ecosystem has settled on MCP the way the web settled on HTTP.

TryMCP on the wire

Step through a real MCP handshake — message by message

Click any message chip to expand the actual JSON-RPC payload. The lane shows direction: client is the MCP host (Cursor, Claude Desktop, your agent); server is your orders service. The full handshake is just eight messages.

Click a message above to inspect its payload.

The inspector walks a connection the way clients opened one before the 2026-07-28 revision, starting with an initialize handshake. The message shapes are still what you will meet in most servers running today.

What changed is that the handshake and the session it created are gone. Each request now carries its own version and capabilities. The stateless flow is spelled out under “Transport and security” below.

Why it exists

Before MCP, the integration matrix looked like this:

N tools  ×  M LLM clients  =  N×M custom integrations

Every new client meant porting every tool. Every new tool meant shipping in every client’s format. Teams ended up maintaining 4-5 copies of the same connector.

MCP collapses it:

N tools  +  M LLM clients  =  N + M  (each speaks MCP)

The shape is the same idea as USB or LSP (Language Server Protocol). Define the protocol once, and the N×M problem becomes N+M.

The architecture

The current MCP spec separates three roles:

  • MCP host — the AI application (Claude Desktop, Claude Code, Cursor, an agent framework). It coordinates one or more MCP clients.
  • MCP client — a component inside the host that maintains a dedicated connection to one MCP server. One client per server.
  • MCP server — exposes capabilities (your tools, your data). Runs as a process the client launches (local) or as an HTTP service it connects to (remote).

A single host typically runs several clients in parallel:

  • one for filesystem access
  • one for GitHub
  • one for your internal database
  • one for Slack

The host stitches them together; the servers don’t know about each other.

MCP hostClaude Desktop / Cursor / LangGraph agentclient 1 (filesystem)client 2 (github)client 3 (your-db)client N…MCP server: filesystemtools • resourcesMCP server: githubtools • promptsMCP server: your-dbtools • resourcesstdiostreamable HTTPstdioone host • many clients • one server per client

The MCP host coordinates a client per server. Each client speaks JSON-RPC over stdio (local) or streamable HTTP (remote).

The three primitives

An MCP server exposes one or more of three things, each with list, get, and (for tools) call JSON-RPC methods:

  • Tools — executable functions the model can call. Same idea as plain tool-calling, but discoverable via MCP instead of hard-coded in the client. query_database, create_issue, send_message. Model-initiated.
  • Resources — read-only data sources the host can pull into context. Files, database rows, API responses. The host decides when to read them. Application-initiated.
  • Prompts — reusable prompt templates the server publishes for the user to invoke (think slash-commands). The server author packages domain expertise — “here’s the right way to ask my server to do X”. User-initiated.

Tools are the workhorse. Most MCP servers are 90% tools, with a few resources for things like “list current open PRs” and prompts for common workflows.

Client-exposed primitives

The spec also defines client-exposed primitives that servers can reach back into. Elicitation — the server asking the user for a missing value or a confirmation mid-call — is the one that survived the 2026-07-28 revision.

It now travels as a multi round-trip request: the server answers with resultType: "input_required", and the client re-sends the same call with the answers in inputResponses.

The 2026-07-28 revision deprecated:

  • Sampling — a server borrowing the host’s LLM
  • Roots — the client declaring which paths a server may touch
  • Logging

They still work, and the spec keeps them for at least twelve months, but don’t build new servers on them — see advanced MCP primitives for what to use instead.

A minimal MCP server in Python

The official Python SDK is the mcp package. FastMCP is the high-level builder. Its shape is striking in how plain it looks: a decorator on a function, and you’re done.

There are official SDKs for TypeScript, Go, C#, Java, and Kotlin too. The four Tier-1 SDKs are:

  • TypeScript
  • Python
  • Go
  • C#

They spoke the 2026-07-28 revision on the day it landed. The Rust SDK is in beta.

# A minimal MCP server with FastMCP.
# In production: pip install mcp, then run with: python server.py
# The real import is: from mcp.server.fastmcp import FastMCP
# Here we mock FastMCP so the shape — decorator on a function — is visible.

class FastMCP:
    def __init__(self, name):
        self.name = name
        self._tools = {}
        self._resources = {}

    def tool(self):
        def deco(fn):
            self._tools[fn.__name__] = fn
            return fn
        return deco

    def resource(self, uri):
        def deco(fn):
            self._resources[uri] = fn
            return fn
        return deco

    def run(self):
        print(f"server '{self.name}' would now listen on stdio")
        print(f"  tools:     {list(self._tools.keys())}")
        print(f"  resources: {list(self._resources.keys())}")

server = FastMCP("orders-server")

# 1. A tool — pydoc becomes the description the LLM reads.
@server.tool()
def search_orders(email: str, limit: int = 10) -> dict:
    """Search a customer's recent orders by email address.

    Use this when the user asks about past orders or order status.
    Returns up to 'limit' orders, newest first.
    """
    # In real code: query your DB
    return {"orders": [{"id": "A-101", "total": 42.00, "status": "shipped"}]}

# 2. Another tool
@server.tool()
def refund_order(order_id: str, reason: str) -> dict:
    """Issue a refund for an order. Requires order_id and a reason string.

    This is a destructive action. The client should confirm with the user
    before invoking.
    """
    return {"order_id": order_id, "refunded": True, "reason": reason}

# 3. A resource — read-only data the client can pull into context
@server.resource("orders://recent")
def recent_orders() -> str:
    """The 10 most recent orders across all customers."""
    return "order A-101 ($42.00), order A-102 ($17.50), ..."

# In a real server you'd call server.run() to start the stdio loop.
server.run()

# Try the tool directly:
print()
print(server._tools["search_orders"]("alex@example.com"))
server 'orders-server' would now listen on stdio
  tools:     ['search_orders', 'refund_order']
  resources: ['orders://recent']

{'orders': [{'id': 'A-101', 'total': 42.0, 'status': 'shipped'}]}

Notice what the decorators bought you: search_orders and refund_order are just normal Python functions, yet the server now knows their names.

The real SDK would turn each docstring and type hint into the JSON schema the LLM reads. Calling search_orders directly returns the same dict an MCP client would get back over the wire.

The framework handles serialization, schema generation, and the wire protocol. You write a function.

To actually run this in production, you’d:

pip install mcp
python server.py

Then in Claude Desktop’s config (or Cursor’s, or wherever), you point at this server. Suddenly every agent in those clients can use your search_orders and refund_order tools.

Transport and security

MCP defines two production transports (the wire protocol the client and server use to exchange JSON-RPC messages):

  • stdio — the client launches the server as a child process and talks to it over stdin/stdout. This is what Claude Desktop, Claude Code, and Cursor use for local servers. Simple, no network exposure.
  • Streamable HTTP — the current spec for remote servers. HTTP POST for client-to-server messages with optional Server-Sent Events for streaming back. The older HTTP+SSE-only transport was deprecated outright in the 2026-07-28 revision; it keeps working through the twelve-month deprecation window, but nothing new should use it.

Stateless by default

Until mid-2026, a remote MCP connection was a session. The client sent initialize, the server answered with its capabilities, and every later request carried an Mcp-Session-Id header back to the same server instance.

The 2026-07-28 revision removed both. In their place:

  • Every request carries its own protocol version, client identity, and client capabilities under _meta — the keys are io.modelcontextprotocol/protocolVersion, /clientInfo, and /clientCapabilities.
  • A server must implement server/discover, which advertises the protocol versions, capabilities, and identity it supports. Discovery is now something you call when you need it, not a handshake you must finish first.
  • Streamable HTTP requests carry Mcp-Method and Mcp-Name headers, so a gateway can route and rate-limit on the operation without parsing the body.
  • These methods return ttlMs and cacheScope, so clients and proxies know how long a listing stays fresh and whether it may be shared:
    • tools/list
    • prompts/list
    • resources/list
    • resources/templates/list
    • resources/read

The payoff is operational: any request can land on any server instance behind a plain round-robin load balancer — no sticky routing, no shared session store.

If your server needs state across calls, it mints an explicit handle (a cart ID, a workflow ID) and takes it back as an ordinary tool argument.

For remote servers, the spec recommends OAuth 2.0 for obtaining bearer tokens, and also allows API keys and custom headers. For stdio servers, auth is process-level — whoever launched the server inherits its capabilities, so any secrets are typically loaded from the host environment or a config file.

Treat any MCP server you write the way you’d treat any API endpoint: scope permissions, rotate tokens, log access.

Why this matters

Three things shifted once MCP took hold:

  • Tool integrations got reusable. A GitHub MCP server written by GitHub itself is now used by Claude Desktop, Cursor, ChatGPT, Windsurf, Zed, and a long tail of agent frameworks. The integration exists once.
  • Internal tools are MCP-ified. Enterprise teams ship MCP servers for their data warehouse, their CRM, their ticket system. Every internal agent — and every developer’s IDE — gets the same access with the same auth story.
  • Agent frameworks adopted it. LangChain, LangGraph, MS Agent Framework, and Google ADK all ship MCP client adapters. You can mix: use LangGraph to orchestrate, point it at three MCP servers, and skip writing connector code entirely.

When you’d write an MCP server vs. just register a tool

It’s still fine — and often correct — to just hand-register tools in your agent code, especially for one-off internal agents. The decision point is reuse:

  • Tool is used in one agent → just register it inline. No MCP needed.
  • Tool is used in multiple agents or clients (IDE, chat, internal bot) → MCP server. Write once, share.
  • Tool wraps a system you don’t own (third-party API) → check if there’s already an MCP server for it. Most popular services have one.

In one breath

  • MCP standardizes how an LLM client connects to a tool provider. Write the integration once as a server, and every MCP client can use it.
  • It turns the N×M connector problem (every tool rebuilt for every client) into N+M. This is the same move USB and LSP made.
  • Architecture: a host runs one client per server. Each server exposes capabilities over JSON-RPC via stdio (local) or Streamable HTTP (remote).
  • Since the 2026-07-28 revision the protocol is stateless — no initialize handshake, no Mcp-Session-Id. Each request carries its version and capabilities in _meta, and server/discover advertises the rest.
  • Three primitives: tools (model-initiated actions), resources (app-initiated read-only data), and prompts (user-initiated templates). Most servers are mostly tools.
  • Reach for a server when a tool is reused across clients. Treat any server you install as untrusted code — it runs with your privileges.

Quick check

Quick check

0/3
Q1What problem does MCP solve?
Q2Your team has built a Slack integration tool used only inside one specific internal agent. Should you bother making it an MCP server?
Q3What are the three core primitives an MCP server can expose?

Wrap-up of the series

You’ve now seen Anthropic’s five workflow patterns plus the agent shape, the evaluator-optimizer loop, and the production tool-calling contract. You’ve also seen the MCP standard that ties the ecosystem together.

From here:

  • Pick a framework (LangChain, LangGraph, MAF, or ADK) and build a tool-using agent against your own data.
  • Wrap your most-used tools as an MCP server so every agent — and every IDE assistant on your team — can use them.
  • Build an eval harness before you ship. The patterns are the easy part; knowing whether your agent is actually working is the hard part.

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
What is the Model Context Protocol (MCP) and what problem does it solve?

MCP is an open protocol originally introduced by Anthropic that standardizes how an AI host discovers and uses tools, resources, and prompt templates exposed by separate servers. It solves bespoke integration sprawl by giving each server one common interface, while leaving model orchestration, permissions, and user approval to the host.

How do MCP tool poisoning, cross-server shadowing, and rug pulls differ?

Tool poisoning is malicious instruction content in a tool description or result; cross-server shadowing uses one server’s names or content to influence or misroute another server’s capability; a rug pull changes a previously reviewed capability later. Defenses combine origin-aware namespaces, isolated trust contexts, capability snapshots and change review, exact-argument authorization, sandboxing, and runtime monitoring.

What is tool use or function calling in LLMs, and how do you design good tools for an agent?

Tool use lets an LLM emit a structured request for an external function, which the application validates, authorizes, executes, and returns to the model. Reliable tools have clear descriptions, narrow scope, strict typed inputs, least-privilege access, idempotency, and useful structured errors.

Why is OAuth token passthrough dangerous in an MCP server?

In a normal MCP deployment, blindly forwarding the client’s bearer token to a downstream API violates the token’s audience boundary and can create confused-deputy, replay, and audit risks. The server should validate a token intended for itself, authorize the tool call, and obtain a separate, narrowly scoped credential for the downstream resource.

Related lessons

Explore further