The Model Context Protocol (MCP) is one of those ideas that sounds abstract until you have hand-written your third bespoke "let the model call our API" integration and realised they are all the same shape. MCP standardises that shape. It is, in plain terms, a protocol that lets an AI application discover and use tools, data, and prompts exposed by external servers — without you writing custom glue for every model and every integration. This guide explains what it is, how the pieces fit, and when it is worth adopting, in language a working developer can act on.
What this guide covers
- The problem MCP solves and why it caught on so fast
- Hosts, clients, and servers — the three roles, explained plainly
- Tools, resources, and prompts — the three things a server exposes
- The transport options (stdio vs HTTP) and when to use each
- A minimal server example and a practical adoption checklist
Info
MCP in one sentence
Think of MCP as the USB-C of AI integrations: one standard plug so any compatible model can talk to any compatible tool or data source, instead of a drawer full of proprietary cables.
The problem MCP solves
Before MCP, connecting a model to your data meant writing custom function-calling code for each model vendor, re-describing your tools in each vendor's format, and maintaining all of it as APIs changed. Five integrations meant five copies of nearly identical glue. MCP replaces the matrix of "M models × N tools" integrations with "M + N": every model speaks MCP, every tool exposes MCP, and they interoperate.
✓ Pros
- Write a connector once, use it from any MCP-aware client
- Tools are discovered at runtime — no hardcoding tool lists
- Clear security boundary between the model and your systems
- A growing ecosystem of ready-made servers (GitHub, Postgres, Slack, files)
✕ Cons
- Another moving part to run, secure, and monitor
- Overkill if you have exactly one model calling one internal function
- The spec is young — expect churn and read the version notes
- Easy to over-expose: a careless server hands the model too much power
The three roles
MCP has exactly three roles, and keeping them straight makes everything else click.
Host
The AI application the user interacts with — an IDE assistant, a desktop chat app, your own agent. The host manages the conversation and decides which servers to connect to.
Client
Lives inside the host and maintains a one-to-one connection to a single server. One host can run many clients, one per server it talks to.
Server
A separate process you write or install that exposes capabilities — tools to call, resources to read, prompts to reuse. The server is where your integration logic lives.
The three things a server exposes
An MCP server can offer three kinds of capability, and the distinction matters because they have different trust implications.
Tools — model-controlled actions
Tools are functions the model can decide to call: create_issue, run_query, send_message. Because the model chooses when to invoke them, tools are the highest-risk capability and deserve the strictest validation and, for anything destructive, human approval.
Resources — application-controlled data
Resources are read-only data the host can pull in as context: a file, a database row, a documentation page. The host (not the model) decides what to load, which makes resources a safer way to ground responses than handing the model a "read anything" tool.
Prompts — user-controlled templates
Prompts are reusable, parameterised templates a server offers — think slash-commands like "summarise this PR" that the user explicitly triggers. They standardise common workflows so every team is not reinventing the same prompt.
Tip
The trust gradient
Notice the pattern: tools are model-controlled, resources are app-controlled, prompts are user-controlled. The more control the model has, the more validation you owe. Design your server so the riskiest capabilities require the most human oversight.
Transports: stdio vs HTTP
MCP servers communicate over one of two transports. Choosing correctly avoids a lot of deployment pain.
stdio ── server runs as a local subprocess of the host
▸ Best for: local tools, file access, desktop assistants
▸ Pros: dead simple, no network, no auth needed
▸ Cons: only local; one process per client
HTTP ── server runs as a remote service (Streamable HTTP)
▸ Best for: shared/team servers, cloud data, SaaS tools
▸ Pros: centralised, multi-client, scalable
▸ Cons: you must handle auth, TLS, and rate limits
A minimal server
Here is the shape of a tiny Python MCP server exposing one tool. The framework handles the protocol; you write the function and its schema.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders")
@mcp.tool()
def get_order_status(order_id: str) -> dict:
"""Return the current status for a given order ID (format ORD-123456)."""
order = lookup_order(order_id) # your existing business logic
if not order:
return {"error": "order not found"}
return {"order_id": order_id, "status": order.status}
if __name__ == "__main__":
mcp.run() # speaks MCP over stdio by default
That is the whole idea: your business logic, wrapped in a typed function, advertised over a standard protocol. Any MCP-aware host — an IDE, a chat client, your own agent — can now discover and call get_order_status without knowing anything about your internals.
A more realistic server: tools and a resource
Real servers expose several capabilities and lean on the trust gradient — model-controlled tools for actions, app-controlled resources for read-only context. Note the validation inside each tool: the arguments come from a model that may be steered by untrusted input, so they are guilty until proven safe.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("orders")
@mcp.tool()
def get_order_status(order_id: str) -> dict:
"""Return the current status for an order (format ORD-123456)."""
if not re.fullmatch(r"ORD-\d{6}", order_id): # validate first, always
return {"error": "invalid order id"}
order = lookup_order(order_id)
return {"order_id": order_id, "status": order.status} if order else {"error": "not found"}
@mcp.tool()
def request_refund(order_id: str, amount_cents: int) -> dict:
"""Queue a refund for human approval. Does NOT move money directly."""
if amount_cents <= 0 or amount_cents > 100_00: # cap the blast radius
return {"error": "amount out of allowed range"}
ticket = RefundQueue.enqueue(order_id, amount_cents) # a human approves later
return {"queued": True, "ticket": ticket.id}
@mcp.resource("orders://{order_id}/summary")
def order_summary(order_id: str) -> str:
"""Read-only context the HOST chooses to load — safer than a 'read anything' tool."""
o = lookup_order(order_id)
return f"Order {order_id}: {o.item_count} items, total ${o.total_dollars}, {o.status}"
if __name__ == "__main__":
mcp.run() # stdio transport by default
Notice request_refund never moves money — it queues a request a human approves. That is the trust gradient in code: the model can propose a consequential action, but the irreversible step stays behind a human gate.
Serving it over HTTP for a team
A local stdio server is one process per user. To share a server across a team or reach cloud data, run it over Streamable HTTP — and now you own authentication. Never expose a tool-bearing MCP server to the network without it.
# Same tools, exposed as a remote service over Streamable HTTP.
mcp = FastMCP("orders", stateless_http=True)
@mcp.tool()
def get_order_status(order_id: str) -> dict: ...
# Wrap with auth middleware — an unauthenticated tool server is a liability.
app = mcp.streamable_http_app()
app.add_middleware(BearerTokenMiddleware, verify=verify_team_token)
# run with: uvicorn server:app --host 0.0.0.0 --port 8080
Using it from a client
On the host side, an MCP-aware client connects, discovers the advertised tools at runtime, and hands them to the model — no tool list hardcoded in your application. The same client code works whether the server is local stdio or remote HTTP.
from mcp import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters
async with stdio_client(StdioServerParameters(command="python", args=["server.py"])) as (r, w):
async with ClientSession(r, w) as session:
await session.initialize()
tools = await session.list_tools() # discovered at runtime, not hardcoded
print([t.name for t in tools.tools]) # ['get_order_status', 'request_refund']
result = await session.call_tool("get_order_status", {"order_id": "ORD-123456"})
print(result.content) # feed this back into the model loop
Warning
Your server is an attack surface
An MCP server can expose powerful capabilities to a model that may be steered by untrusted input (see prompt injection). Scope each server narrowly, validate every argument, never expose raw shell or unrestricted SQL, and require explicit approval for writes. A convenient server that can "do anything" is a liability waiting to happen.
When to adopt MCP
MCP is worth it the moment you have more than one model talking to more than one system, or you want to reuse community connectors instead of building your own. If you have exactly one agent calling one internal function, plain function calling is simpler and fine. The break-even comes fast in any real organisation, because integrations multiply.
integrations with MCP, versus M × N without it
Pro tip
Before writing your own server, check the ecosystem — there are maintained MCP servers for GitHub, Postgres, filesystems, Slack, and many SaaS tools. Reusing one is faster, and you inherit its security review. Build custom only for your proprietary systems.
A practical adoption checklist
✓ Pros
- Start with one read-only server (resources) to prove the wiring
- Add tools one at a time, each with a strict input schema
- Use stdio for local/desktop, HTTP with auth for shared servers
- Gate every write behind explicit human approval
- Pin the MCP SDK version and track spec changes
✕ Cons
- No unauthenticated HTTP servers exposed to the internet
- No raw shell or unrestricted SQL tools, ever
- No "expose the whole database" resource handlers
- No skipping argument validation because "the model is smart"
The bottom line
MCP is not hype — it is plumbing, and good plumbing is exactly what the agent ecosystem needed. It will not make your AI smarter, but it will stop you from rewriting the same connector for the fifth time and give you a clean, auditable boundary between the model and your systems. Learn the three roles, respect the trust gradient, and adopt it the moment your integration count starts to multiply.
! Common mistakes to avoid
-
✕Exposing a raw shell or unrestricted SQL tool over MCP.
✓Expose narrow, purpose-built tools with strict schemas; never a "run anything" capability.
-
✕Running an HTTP MCP server with no authentication.
✓Require auth and TLS on any networked server; treat it as a public attack surface.
-
✕Skipping argument validation because "the model is smart".
✓Validate every tool argument server-side — the model can be steered by untrusted input.
-
✕Building a custom server for a system that already has one.
✓Check the ecosystem first (GitHub, Postgres, Slack, filesystem) and reuse maintained servers.
? Frequently asked questions
What problem does MCP actually solve? +
It replaces the M-models-by-N-tools matrix of bespoke integrations with M+N: every model speaks MCP and every tool exposes MCP, so connectors are written once and reused everywhere.
What is the difference between tools, resources, and prompts? +
Tools are model-controlled actions, resources are app-controlled read-only data, and prompts are user-controlled templates. The more control the model has, the more validation you owe.
Should I use stdio or HTTP transport? +
Use stdio for local, single-user tools (desktop assistants, file access) — it needs no auth. Use Streamable HTTP for shared/team or cloud servers, where you must add authentication and TLS.
Is MCP only for one AI vendor? +
No. MCP is an open protocol; any MCP-aware host or client can use any MCP server, which is the entire point of standardising the integration layer.
Do I need MCP if I have one agent calling one function? +
No. Plain function calling is simpler for a single integration. MCP pays off the moment you have multiple models or multiple systems, where integrations start to multiply.
Success
The payoff
Once your tools live behind MCP, swapping models or adding a new agent stops being an integration project and becomes a configuration change. That decoupling is the whole point — and it compounds with every connector you add.