The Four Big Ideas
The four ideas that make MCP click: one protocol instead of N integrations, three simple primitives, a safe boundary around your data, and total model- and vendor-independence.
You already know the one-liner: MCP is a USB-C port for AI — one connector for tools and information. But why does that matter, and why is everyone excited about it? It comes down to four ideas. Once these click, the rest of MCP is just details.
#Idea 1 — One protocol, not N integrations
Before MCP, every agent needed custom code for every tool. Three agents times four tools meant twelve bespoke integrations — and adding a new tool meant rewriting glue for all of them. Integrations exploded combinatorially.
MCP is a single open standard. Write a connector once, and any MCP-aware agent can use it. Each side implements MCP one time; after that, any agent talks to any tool through the same protocol.
The M×N problem
Before MCP: M agents × N tools = chaos. Every pairing needs bespoke glue.
With MCP: everyone speaks one protocol. Add a tool and every agent can use it instantly — no new glue.
#Idea 2 — Three simple primitives
A server's entire surface area is just three things:
- Tools — actions the model can invoke (run a query, send a message, create a file).
- Resources — read-only context the agent can pull in (a schema, a doc, config).
- Prompts — reusable templates for common interactions.
That's it. That's the whole surface area. Here's a real server exposing a tool and a resource — the two you'll use most. Note the split of responsibility: the agent decides when to call query_sales; your code decides what it does, and the agent never touches the database directly.
from mcp.server.fastmcp import FastMCP
import sqlite3
db = sqlite3.connect("shop.db", check_same_thread=False)
# 1. Create the server — your MCP "connector".
mcp = FastMCP("sales-connector")
# 2. Expose a TOOL. The decorator + type hints become the schema
# the agent reads to know how to call this function.
@mcp.tool()
def query_sales(metric: str, group_by: str = "customer", limit: int = 5) -> list[dict]:
"""Aggregate revenue by customer or by month."""
if group_by == "month":
sql = ("SELECT strftime('%Y-%m', created_at) AS month, "
"SUM(amount) AS revenue FROM orders "
"GROUP BY month ORDER BY month LIMIT ?")
else:
sql = ("SELECT c.name AS customer, SUM(o.amount) AS total "
"FROM orders o JOIN customers c ON c.id = o.customer_id "
"GROUP BY c.name ORDER BY total DESC LIMIT ?")
cur = db.execute(sql, (limit,))
cols = [d[0] for d in cur.description]
return [dict(zip(cols, row)) for row in cur.fetchall()]
# 3. Expose a RESOURCE — read-only context the agent can pull in.
@mcp.resource("schema://shop")
def schema() -> str:
"""The database schema the model can reason over."""
return "orders(id, customer_id, amount, created_at)"
# 4. Run over stdio so a host like Claude can launch and talk to it.
if __name__ == "__main__":
mcp.run()Which of these is the complete set of MCP server primitives?
#Idea 3 — A safe boundary
This is the idea that makes MCP safe to put in front of real systems. The model never holds your credentials. Your server sits in the middle as a controlled boundary: it validates every input, can enforce read-only access, scope which tables are visible, and audit every call.
Even an "escape hatch" tool that lets the model write raw SQL stays safe, because the server is the boundary — it inspects and rejects anything dangerous before it ever touches the database:
@mcp.tool()
def run_sql(sql: str) -> str:
"""Escape hatch: run a single read-only SELECT. The server is the boundary."""
clean = sql.strip().rstrip(";")
if not clean.lower().startswith("select") or ";" in clean:
return "Error: only a single read-only SELECT is allowed."
try:
return _rows(db.execute(clean))
except sqlite3.Error as exc: # pragma: no cover - defensive
return f"Error: {exc}"Why is exposing a database through an MCP server safer than handing the model the raw DB credentials?
#Idea 4 — Model- & vendor-agnostic
Because MCP is an open protocol, the same server works with Claude, an IDE, or your own homegrown agent. You can swap the model without rewriting a single connector. To connect it, you just point a host at the server — the host launches it and the connection is live:
{
"mcpServers": {
"sales-connector": {
"command": "node",
"args": ["/absolute/path/to/server.js"]
},
"sales-connector-python": {
"command": "uv",
"args": ["run", "server.py"]
}
}
}The payoff
Write your connector once (Idea 1), expose it through three primitives (Idea 2), keep your data behind a validating boundary (Idea 3), and plug it into any model or tool you like (Idea 4). That's the whole promise of MCP.
Key takeaways
- MCP is one open protocol: write a connector once and any MCP-aware agent can use it, instead of building M×N bespoke integrations.
- A server's entire surface area is three primitives — Tools (actions), Resources (read-only context), and Prompts (reusable templates).
- The server is a safe boundary: the model never holds your credentials, and your code validates inputs, enforces read-only access, scopes visibility, and audits every call.
- MCP is model- and vendor-agnostic — the same server works with Claude, an IDE, or your own agent, so you can swap the model without rewriting the connector.
A user asks the agent for churn-risk customers. The agent sends this over the wire to your server. Which primitive is being used, and who decided to send it?
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "query_sales",
"arguments": { "metric": "recency", "limit": 3 } } }This run_sql tool is supposed to be a safe boundary that only allows a single read-only SELECT. But it lets a destructive statement through. Which fix restores the boundary?
@mcp.tool()
def run_sql(sql: str) -> str:
"""Escape hatch: run a single read-only SELECT."""
clean = sql.strip().rstrip(";")
# BUG: no validation happens here
return _rows(db.execute(clean))Complete this FastMCP server so it exposes an action the model can invoke and a piece of read-only context.
mcp = FastMCP("sales-connector") @mcp.() def query_sales(metric: str) -> list[dict]: """Aggregate revenue by customer or by month.""" ... @mcp.("schema://shop") def schema() -> str: """The database schema the model can reason over.""" return "orders(id, customer_id, amount, created_at)"
Put the four MCP architecture layers in order, from the app the user talks to, all the way down to the real data.
MCP Server — your connector code; advertises Tools, Resources & Prompts and validates inputs
Host — the app the user interacts with; embeds the LLM and one MCP client per server
MCP Client — lives inside the host; speaks JSON-RPC 2.0 and relays tool calls
Data / Service — the actual DB, API, or filesystem the server gatekeeps
You're designing an MCP connector for a company's HR system. Sketch (in prose or pseudo-config) how you'd apply all four big ideas:
- One protocol — name one MCP-aware host that could reuse this connector without any custom glue.
- Three primitives — list one Tool, one Resource, and one Prompt you would expose.
- Safe boundary — describe one input your server would validate or reject, and one thing it would keep read-only or hidden from the model.
- Vendor-agnostic — explain what you'd have to change to swap the underlying model from Claude to your own agent.
Try it yourself — a starting point to build on:
# Write your solution here