September 12, 2025
The USB-C of AI: Model Context Protocol (MCP) Tutorial + Security Playbook (Python)
Build once, connect everywhere: Python + Model Context Protocol (MCP).

By Tarun Singh
10 min read
Model Context Protocol (MCP) is quickly becoming the universal connector for AI tools. Instead of building one-off plugins for every assistant, IDE, or agent, you expose your tools once through Model Context Protocol (MCP) and let compatible clients discover, call, and reason over them. Think USB-C for AI apps: one standard, many devices.
This copy-paste-ready guide gives you three things:
- a clear, simple explanation of Model Context Protocol (MCP),
- a production-leaning Python MCP server you can run today, and
- a practical security playbook (allow-lists, OAuth-ready hooks, audits, content firewalls) so your Model Context Protocol (MCP) tools don't become attack paths.
The language is intentionally simple. The code is intentionally clean. The focus keyword is intentionally consistent: Model Context Protocol (MCP).
TL;DR
- Model Context Protocol (MCP) standardizes how AI clients call external tools.
- You'll build a minimal Python MCP server (stdio JSON) exposing three real tools:
search,db_query,file_read. - You'll harden it with allow-lists, read-only DB, timeouts, rate limits, audits, and a content firewall.
- You'll get a drop-in
mcp.jsonso compatible clients can discover your server.
Why Model Context Protocol (MCP) matters
AI systems keep improving at reasoning, but integration friction still slows teams down. The answer is Model Context Protocol (MCP):
- Portability: write your tools once; connect to many AI clients.
- Composability: combine tools (search, vector DB, billing, file ops) behind a single Model Context Protocol (MCP) endpoint.
- Governance: standardize policies, logging, and approvals across teams.
Once your data and actions live behind Model Context Protocol (MCP), you stop rewriting integrations and start shipping features.
๐ฅ Want to run an LLM right inside your browser โ totally free? ๐ Check out the tutorial video below ๐ ๐ซ
The mental model (simple diagram)
The AI client speaks Model Context Protocol (MCP) to discover tools and call them. Your server translates those calls into real actions with policies and logs.
What Model Context Protocol (MCP) is (and isn't)
Model Context Protocol (MCP) is:
- A standard way to list tools, describe inputs/outputs, and call tools with structured messages.
- Transport-agnostic (stdio, sockets, HTTP, etc.).
- Friendly to policy enforcement and auditing because the surface is predictable.
Model Context Protocol (MCP) isn't:
- Magic security. You still need allow-lists, auth, and logging.
- A full app framework. It's a protocol for connecting tools to AI clients.
What we'll build
A Python Model Context Protocol (MCP) server that:
- serves three safe, useful tools:
search(query)โ demo search over a local mini-index,db_query(sql, params)โ read-only SQLite queries,file_read(path)โ read files only from approved folders,- enforces allow-lists, read-only DB, timeouts, rate limits, and audited calls,
- speaks a simple JSON-RPC-style contract over stdio so MCP-capable clients can connect,
- ships with a tiny content firewall to block obvious prompt-injection and dangerous payloads.
Copy the code block below into
server.pyand run it with Python 3.10+.
Full code: Python Model Context Protocol (MCP) server (secure defaults)
#!/usr/bin/env python3
# server.py
# Minimal, production-leaning Model Context Protocol (MCP) server over stdio.
# Tools: search (local), db_query (SQLite read-only), file_read (allow-listed).
# Security: allow-lists, read-only DB, timeouts, rate limits, audits, content firewall.
import sys, os, json, time, sqlite3, threading, queue, re
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
# -----------------------------
# Configuration (env overrides)
# -----------------------------
ALLOWED_DIRS = [Path(p).resolve() for p in os.getenv("MCP_ALLOWED_DIRS", ".").split(os.pathsep)]
SQLITE_PATH = os.getenv("MCP_SQLITE_PATH", "mcp_demo.db")
AUDIT_PATH = os.getenv("MCP_AUDIT_PATH", "audit.log")
RATE_CALLS = int(os.getenv("MCP_RATE_CALLS", "30")) # calls per window
RATE_WINDOW = float(os.getenv("MCP_RATE_WINDOW", "60.0")) # seconds
# Demo local "search index" (swap with your own indexer later)
LOCAL_DOCS = [
{"id":"guide", "title":"Model Context Protocol (MCP) Guide", "text":"MCP connects AI clients to tools safely."},
{"id":"security","title":"MCP Security Checklist", "text":"Allow-lists, audits, and timeouts reduce risk."},
{"id":"python", "title":"Python MCP Server", "text":"A stdio JSON server exposes MCP tools."},
]
# -----------------------------
# Utilities
# -----------------------------
def to_json(obj: Any) -> str:
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
def now_ms() -> int:
return int(time.time() * 1000)
def normalize_path(p: str) -> Path:
return Path(p).expanduser().resolve()
def path_is_allowed(p: Path) -> bool:
try:
return any(str(p).startswith(str(base)) for base in ALLOWED_DIRS)
except Exception:
return False
# -----------------------------
# Content Firewall (very simple)
# -----------------------------
INJECTION_PATTERNS = [
r"(?i)\bignore\s+all\s+previous\s+instructions\b",
r"(?i)\bexfiltrate\b",
r"(?i)\brm\s+-rf\b",
r"(?i)\bshutdown\b",
r"(?i)\bdisable\s+security\b",
r"(?i)\bLOAD\s+DATA\b",
]
def content_firewall(payload: Dict[str, Any]) -> Optional[str]:
"""
Return reason string if blocked, else None.
Checks tool name, params strings for dangerous intents.
"""
s = to_json(payload)
for pat in INJECTION_PATTERNS:
if re.search(pat, s):
return f"Blocked by content firewall: pattern '{pat}'"
return None
# -----------------------------
# Rate Limiter (token bucket)
# -----------------------------
class RateLimiter:
def __init__(self, calls: int, window: float):
self.calls = calls
self.window = window
self.lock = threading.Lock()
self.events: List[float] = []
def allow(self) -> bool:
with self.lock:
t = time.time()
self.events = [e for e in self.events if t - e <= self.window]
if len(self.events) < self.calls:
self.events.append(t)
return True
return False
limiter = RateLimiter(RATE_CALLS, RATE_WINDOW)
# -----------------------------
# Audit Logger (JSON Lines)
# -----------------------------
audit_lock = threading.Lock()
def audit(event: Dict[str, Any]) -> None:
event["ts"] = now_ms()
with audit_lock, open(AUDIT_PATH, "a", encoding="utf-8") as f:
f.write(to_json(event) + "\n")
# -----------------------------
# DB (SQLite read-only)
# -----------------------------
def connect_sqlite_ro(db_path: str) -> sqlite3.Connection:
uri = f"file:{Path(db_path).resolve()}?mode=ro"
return sqlite3.connect(uri, uri=True, check_same_thread=False)
def run_query_ro(conn: sqlite3.Connection, sql: str, params: List[Any]) -> List[Dict[str, Any]]:
sql_stripped = sql.strip().upper()
if not sql_stripped.startswith("SELECT"):
raise ValueError("Only SELECT queries are allowed in read-only mode.")
cur = conn.execute(sql, params or [])
cols = [c[0] for c in cur.description]
rows = [dict(zip(cols, r)) for r in cur.fetchall()]
return rows
# -----------------------------
# Tool Implementations
# -----------------------------
def tool_search(params: Dict[str, Any]) -> Dict[str, Any]:
q = (params.get("query") or "").strip().lower()
if not q:
return {"results": []}
scored = []
for doc in LOCAL_DOCS:
text = f"{doc['title']} {doc['text']}".lower()
score = sum(1 for w in q.split() if w in text)
if score > 0:
scored.append((score, doc))
scored.sort(key=lambda x: x[0], reverse=True)
return {"results": [d for _, d in scored[:10]]}
def tool_db_query(params: Dict[str, Any]) -> Dict[str, Any]:
sql = params.get("sql") or ""
query_params = params.get("params") or []
conn = connect_sqlite_ro(SQLITE_PATH)
try:
rows = run_query_ro(conn, sql, query_params)
return {"rows": rows, "count": len(rows)}
finally:
conn.close()
def tool_file_read(params: Dict[str, Any]) -> Dict[str, Any]:
path = normalize_path(params.get("path") or "")
if not path_is_allowed(path):
raise PermissionError("Path not allowed by server policy.")
if not path.exists() or not path.is_file():
raise FileNotFoundError("File not found.")
# size limit (1 MB)
if path.stat().st_size > 1_000_000:
raise ValueError("File too large; max 1MB.")
with open(path, "r", encoding="utf-8", errors="ignore") as f:
return {"path": str(path), "content": f.read()}
TOOLS = {
"search": {
"description": "Local keyword search over a small demo index.",
"input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
"fn": tool_search,
},
"db_query": {
"description": "Read-only SQLite SELECT query.",
"input_schema": {
"type": "object",
"properties": {
"sql": {"type": "string"},
"params": {"type": "array"}
},
"required": ["sql"]
},
"fn": tool_db_query,
},
"file_read": {
"description": "Read text files from approved folders.",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"]
},
"fn": tool_file_read,
}
}
# Allow-list which tools are callable (override via env)
ALLOWED_TOOLS = set((os.getenv("MCP_ALLOWED_TOOLS") or ",".join(TOOLS.keys())).split(","))
# -----------------------------
# JSON-RPC-ish Stdio Transport
# -----------------------------
INQ: "queue.Queue[str]" = queue.Queue()
def reader():
for line in sys.stdin:
line = line.strip()
if line:
INQ.put(line)
threading.Thread(target=reader, daemon=True).start()
def respond(id_: Any, result: Any=None, error: Any=None):
msg = {"jsonrpc":"2.0","id":id_}
if error is not None:
msg["error"] = {"message": str(error)}
else:
msg["result"] = result
sys.stdout.write(to_json(msg) + "\n")
sys.stdout.flush()
def list_tools_payload():
return {
"protocol": "mcp-stdio-json",
"tools": [
{"name": name, "description": meta["description"], "input_schema": meta["input_schema"]}
for name, meta in TOOLS.items() if name in ALLOWED_TOOLS
],
}
def handle_call(req: Dict[str, Any]):
req_id = req.get("id")
method = req.get("method")
params = req.get("params") or {}
# Rate-limit check
if not limiter.allow():
audit({"event": "rate_limit", "method": method, "params": params})
return respond(req_id, error="Rate limit exceeded. Try later.")
# Content firewall
blocked = content_firewall({"method": method, "params": params})
if blocked:
audit({"event":"blocked", "reason":blocked, "method":method, "params":params})
return respond(req_id, error=blocked)
try:
if method == "ping":
return respond(req_id, {"ok": True, "ts": now_ms()})
if method == "list_tools":
return respond(req_id, list_tools_payload())
if method == "call_tool":
name = params.get("name")
args = params.get("arguments") or {}
if name not in TOOLS or name not in ALLOWED_TOOLS:
raise ValueError("Tool not allowed.")
fn = TOOLS[name]["fn"]
# Timeout guard
result_box: List[Any] = []
exc_box: List[BaseException] = []
def runner():
try:
result_box.append(fn(args))
except BaseException as e:
exc_box.append(e)
t = threading.Thread(target=runner, daemon=True)
t.start(); t.join(timeout=8.0)
if t.is_alive():
audit({"event":"timeout", "tool":name, "args":args})
return respond(req_id, error="Tool call timed out.")
if exc_box:
raise exc_box[0]
audit({"event":"tool_call", "tool":name, "args":args, "ok":True})
return respond(req_id, {"tool": name, "data": result_box[0]})
# Unknown
return respond(req_id, error="Unknown method.")
except Exception as ex:
audit({"event":"error", "method":method, "params":params, "error":str(ex)})
return respond(req_id, error=str(ex))
def main():
# Warm up: ensure DB exists (demo)
dbf = Path(SQLITE_PATH)
if not dbf.exists():
conn = sqlite3.connect(dbf)
try:
conn.executescript("""
CREATE TABLE IF NOT EXISTS employees(id INTEGER PRIMARY KEY, name TEXT, role TEXT);
INSERT INTO employees(name, role) VALUES
('Asha','Engineer'), ('Ben','Analyst'), ('Chen','Designer');
""")
conn.commit()
finally:
conn.close()
# Announce readiness
sys.stdout.write(to_json({"jsonrpc":"2.0","method":"ready","params":list_tools_payload()}) + "\n")
sys.stdout.flush()
# Event loop
while True:
try:
line = INQ.get()
req = json.loads(line)
handle_call(req)
except KeyboardInterrupt:
break
except Exception as e:
# best-effort error
sys.stdout.write(to_json({"jsonrpc":"2.0","method":"server_error","params":{"error":str(e)}}) + "\n")
sys.stdout.flush()
if __name__ == "__main__":
main()#!/usr/bin/env python3
# server.py
# Minimal, production-leaning Model Context Protocol (MCP) server over stdio.
# Tools: search (local), db_query (SQLite read-only), file_read (allow-listed).
# Security: allow-lists, read-only DB, timeouts, rate limits, audits, content firewall.
import sys, os, json, time, sqlite3, threading, queue, re
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
# -----------------------------
# Configuration (env overrides)
# -----------------------------
ALLOWED_DIRS = [Path(p).resolve() for p in os.getenv("MCP_ALLOWED_DIRS", ".").split(os.pathsep)]
SQLITE_PATH = os.getenv("MCP_SQLITE_PATH", "mcp_demo.db")
AUDIT_PATH = os.getenv("MCP_AUDIT_PATH", "audit.log")
RATE_CALLS = int(os.getenv("MCP_RATE_CALLS", "30")) # calls per window
RATE_WINDOW = float(os.getenv("MCP_RATE_WINDOW", "60.0")) # seconds
# Demo local "search index" (swap with your own indexer later)
LOCAL_DOCS = [
{"id":"guide", "title":"Model Context Protocol (MCP) Guide", "text":"MCP connects AI clients to tools safely."},
{"id":"security","title":"MCP Security Checklist", "text":"Allow-lists, audits, and timeouts reduce risk."},
{"id":"python", "title":"Python MCP Server", "text":"A stdio JSON server exposes MCP tools."},
]
# -----------------------------
# Utilities
# -----------------------------
def to_json(obj: Any) -> str:
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
def now_ms() -> int:
return int(time.time() * 1000)
def normalize_path(p: str) -> Path:
return Path(p).expanduser().resolve()
def path_is_allowed(p: Path) -> bool:
try:
return any(str(p).startswith(str(base)) for base in ALLOWED_DIRS)
except Exception:
return False
# -----------------------------
# Content Firewall (very simple)
# -----------------------------
INJECTION_PATTERNS = [
r"(?i)\bignore\s+all\s+previous\s+instructions\b",
r"(?i)\bexfiltrate\b",
r"(?i)\brm\s+-rf\b",
r"(?i)\bshutdown\b",
r"(?i)\bdisable\s+security\b",
r"(?i)\bLOAD\s+DATA\b",
]
def content_firewall(payload: Dict[str, Any]) -> Optional[str]:
"""
Return reason string if blocked, else None.
Checks tool name, params strings for dangerous intents.
"""
s = to_json(payload)
for pat in INJECTION_PATTERNS:
if re.search(pat, s):
return f"Blocked by content firewall: pattern '{pat}'"
return None
# -----------------------------
# Rate Limiter (token bucket)
# -----------------------------
class RateLimiter:
def __init__(self, calls: int, window: float):
self.calls = calls
self.window = window
self.lock = threading.Lock()
self.events: List[float] = []
def allow(self) -> bool:
with self.lock:
t = time.time()
self.events = [e for e in self.events if t - e <= self.window]
if len(self.events) < self.calls:
self.events.append(t)
return True
return False
limiter = RateLimiter(RATE_CALLS, RATE_WINDOW)
# -----------------------------
# Audit Logger (JSON Lines)
# -----------------------------
audit_lock = threading.Lock()
def audit(event: Dict[str, Any]) -> None:
event["ts"] = now_ms()
with audit_lock, open(AUDIT_PATH, "a", encoding="utf-8") as f:
f.write(to_json(event) + "\n")
# -----------------------------
# DB (SQLite read-only)
# -----------------------------
def connect_sqlite_ro(db_path: str) -> sqlite3.Connection:
uri = f"file:{Path(db_path).resolve()}?mode=ro"
return sqlite3.connect(uri, uri=True, check_same_thread=False)
def run_query_ro(conn: sqlite3.Connection, sql: str, params: List[Any]) -> List[Dict[str, Any]]:
sql_stripped = sql.strip().upper()
if not sql_stripped.startswith("SELECT"):
raise ValueError("Only SELECT queries are allowed in read-only mode.")
cur = conn.execute(sql, params or [])
cols = [c[0] for c in cur.description]
rows = [dict(zip(cols, r)) for r in cur.fetchall()]
return rows
# -----------------------------
# Tool Implementations
# -----------------------------
def tool_search(params: Dict[str, Any]) -> Dict[str, Any]:
q = (params.get("query") or "").strip().lower()
if not q:
return {"results": []}
scored = []
for doc in LOCAL_DOCS:
text = f"{doc['title']} {doc['text']}".lower()
score = sum(1 for w in q.split() if w in text)
if score > 0:
scored.append((score, doc))
scored.sort(key=lambda x: x[0], reverse=True)
return {"results": [d for _, d in scored[:10]]}
def tool_db_query(params: Dict[str, Any]) -> Dict[str, Any]:
sql = params.get("sql") or ""
query_params = params.get("params") or []
conn = connect_sqlite_ro(SQLITE_PATH)
try:
rows = run_query_ro(conn, sql, query_params)
return {"rows": rows, "count": len(rows)}
finally:
conn.close()
def tool_file_read(params: Dict[str, Any]) -> Dict[str, Any]:
path = normalize_path(params.get("path") or "")
if not path_is_allowed(path):
raise PermissionError("Path not allowed by server policy.")
if not path.exists() or not path.is_file():
raise FileNotFoundError("File not found.")
# size limit (1 MB)
if path.stat().st_size > 1_000_000:
raise ValueError("File too large; max 1MB.")
with open(path, "r", encoding="utf-8", errors="ignore") as f:
return {"path": str(path), "content": f.read()}
TOOLS = {
"search": {
"description": "Local keyword search over a small demo index.",
"input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
"fn": tool_search,
},
"db_query": {
"description": "Read-only SQLite SELECT query.",
"input_schema": {
"type": "object",
"properties": {
"sql": {"type": "string"},
"params": {"type": "array"}
},
"required": ["sql"]
},
"fn": tool_db_query,
},
"file_read": {
"description": "Read text files from approved folders.",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"]
},
"fn": tool_file_read,
}
}
# Allow-list which tools are callable (override via env)
ALLOWED_TOOLS = set((os.getenv("MCP_ALLOWED_TOOLS") or ",".join(TOOLS.keys())).split(","))
# -----------------------------
# JSON-RPC-ish Stdio Transport
# -----------------------------
INQ: "queue.Queue[str]" = queue.Queue()
def reader():
for line in sys.stdin:
line = line.strip()
if line:
INQ.put(line)
threading.Thread(target=reader, daemon=True).start()
def respond(id_: Any, result: Any=None, error: Any=None):
msg = {"jsonrpc":"2.0","id":id_}
if error is not None:
msg["error"] = {"message": str(error)}
else:
msg["result"] = result
sys.stdout.write(to_json(msg) + "\n")
sys.stdout.flush()
def list_tools_payload():
return {
"protocol": "mcp-stdio-json",
"tools": [
{"name": name, "description": meta["description"], "input_schema": meta["input_schema"]}
for name, meta in TOOLS.items() if name in ALLOWED_TOOLS
],
}
def handle_call(req: Dict[str, Any]):
req_id = req.get("id")
method = req.get("method")
params = req.get("params") or {}
# Rate-limit check
if not limiter.allow():
audit({"event": "rate_limit", "method": method, "params": params})
return respond(req_id, error="Rate limit exceeded. Try later.")
# Content firewall
blocked = content_firewall({"method": method, "params": params})
if blocked:
audit({"event":"blocked", "reason":blocked, "method":method, "params":params})
return respond(req_id, error=blocked)
try:
if method == "ping":
return respond(req_id, {"ok": True, "ts": now_ms()})
if method == "list_tools":
return respond(req_id, list_tools_payload())
if method == "call_tool":
name = params.get("name")
args = params.get("arguments") or {}
if name not in TOOLS or name not in ALLOWED_TOOLS:
raise ValueError("Tool not allowed.")
fn = TOOLS[name]["fn"]
# Timeout guard
result_box: List[Any] = []
exc_box: List[BaseException] = []
def runner():
try:
result_box.append(fn(args))
except BaseException as e:
exc_box.append(e)
t = threading.Thread(target=runner, daemon=True)
t.start(); t.join(timeout=8.0)
if t.is_alive():
audit({"event":"timeout", "tool":name, "args":args})
return respond(req_id, error="Tool call timed out.")
if exc_box:
raise exc_box[0]
audit({"event":"tool_call", "tool":name, "args":args, "ok":True})
return respond(req_id, {"tool": name, "data": result_box[0]})
# Unknown
return respond(req_id, error="Unknown method.")
except Exception as ex:
audit({"event":"error", "method":method, "params":params, "error":str(ex)})
return respond(req_id, error=str(ex))
def main():
# Warm up: ensure DB exists (demo)
dbf = Path(SQLITE_PATH)
if not dbf.exists():
conn = sqlite3.connect(dbf)
try:
conn.executescript("""
CREATE TABLE IF NOT EXISTS employees(id INTEGER PRIMARY KEY, name TEXT, role TEXT);
INSERT INTO employees(name, role) VALUES
('Asha','Engineer'), ('Ben','Analyst'), ('Chen','Designer');
""")
conn.commit()
finally:
conn.close()
# Announce readiness
sys.stdout.write(to_json({"jsonrpc":"2.0","method":"ready","params":list_tools_payload()}) + "\n")
sys.stdout.flush()
# Event loop
while True:
try:
line = INQ.get()
req = json.loads(line)
handle_call(req)
except KeyboardInterrupt:
break
except Exception as e:
# best-effort error
sys.stdout.write(to_json({"jsonrpc":"2.0","method":"server_error","params":{"error":str(e)}}) + "\n")
sys.stdout.flush()
if __name__ == "__main__":
main()How it works:
- The server announces a
readyevent with a list of tools (a typical Model Context Protocol (MCP) pattern). - Clients send JSON messages like:
{"jsonrpc":"2.0","id":1,"method":"list_tools"}{"jsonrpc":"2.0","id":2,"method":"call_tool","params":{"name":"search","arguments":{"query":"mcp"}}}- The server enforces rate limits, content firewall, timeouts, allow-lists, read-only DB, and audit logs in
audit.log.
Run it locally
python3 server.pypython3 server.pyOptional env overrides:
# allow only file_read and search
export MCP_ALLOWED_TOOLS="file_read,search"
# restrict file access to ./docs and ./snippets
export MCP_ALLOWED_DIRS="./docs:./snippets"
# use a different SQLite path (read-only mode is enforced at call time)
export MCP_SQLITE_PATH="example.db"# allow only file_read and search
export MCP_ALLOWED_TOOLS="file_read,search"
# restrict file access to ./docs and ./snippets
export MCP_ALLOWED_DIRS="./docs:./snippets"
# use a different SQLite path (read-only mode is enforced at call time)
export MCP_SQLITE_PATH="example.db"Tell clients how to find your Model Context Protocol (MCP) server
Create a minimal mcp.json adjacent to your project (clients read this file to auto-discover servers):
{
"mcpServers": {
"my-mcp-tools": {
"command": "python3",
"args": ["server.py"],
"env": {
"MCP_ALLOWED_DIRS": "./docs:./snippets"
}
}
}
}{
"mcpServers": {
"my-mcp-tools": {
"command": "python3",
"args": ["server.py"],
"env": {
"MCP_ALLOWED_DIRS": "./docs:./snippets"
}
}
}
}Any MCP-capable client that looks for mcp.json can now discover my-mcp-tools, start the command, and call tools using Model Context Protocol (MCP) messages over stdio.
๐ฅ Want to run an LLM right inside your browser โ totally free? ๐ Check out the tutorial video below ๐ ๐ซ
Security playbook for Model Context Protocol (MCP)
Security is where most "hello world" posts stop. We won't.
1) Identity & permissions
- Use short-lived tokens when you expose Model Context Protocol (MCP) over a socket/HTTP proxy.
- Scope tokens per tool: search-only, read-only DB, read-files.
- Tie scopes to an allow-list inside your server (
MCP_ALLOWED_TOOLS).
2) Data boundaries
- Enforce read-only DB connections and SELECT-only queries.
- Validate and normalize file paths; only allow approved directories.
- Cap response sizes and execution time (already in the server).
3) Content firewall
- Block obvious injection patterns before tool execution.
- Require user approval for risky actions (e.g., file write, network posts).
- Log why something was blocked (already in the server).
4) Rate limiting and backpressure
- Token buckets per client (already included).
- Global limits for expensive tools (vector search, LLM calls).
5) Auditing
- Log every tool call with timestamp, tool name, parameters (hashed if sensitive), and outcome.
- Keep JSON Lines (
audit.log) so you can stream to SIEM later.
6) Supply chain hygiene
- Pin versions, sign releases, and generate an SBOM for any external deps.
- Run a daily "advisory" scan and rotate secrets on automation.
7) Approvals & break-glass
- Wrap dangerous tools with a two-step approval flow: request โ human approve โ execute.
- Keep a "break-glass" emergency path with extra logging and alerts.
8) Zero-trust posture for Model Context Protocol (MCP)
- Treat every tool call as hostile until proven safe.
- Default-deny; explicitly allow operations and paths.
- Prefer idempotent tools and design for replay-safety.
Extending the server (production patterns)
- OAuth2/OIDC: put a thin HTTP proxy in front of the stdio server; validate tokens and inject identity/scopes as env vars.
- Remote MCP: expose TCP/WebSocket transport; terminate TLS at the edge; forward to stdio.
- Vector search: add a
semantic_search(query, top_k)tool with your favorite store; keep budgets and timeouts. - Human-in-the-loop: an
approve(action_id)tool that an internal approver calls from an admin client. - Metrics: latency per tool, error rates, blocked reasons โ export Prometheus counters.
Quick tests (copy/paste)
Open a second terminal and send a request manually:
# list tools
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"list_tools"}' | python3 server.py# list tools
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"list_tools"}' | python3 server.pyOr, if the server is already running, you can adapt this idea using a simple named pipe or wrap the server with an HTTP shim for local testing. The important part: you're speaking Model Context Protocol (MCP) style messages โ list tools, then call tools.
FAQs
Q: What is Model Context Protocol (MCP) in one sentence? A: Model Context Protocol (MCP) is a standard way for AI clients to discover and call your tools with structured messages and predictable policies.
Q: Is Model Context Protocol (MCP) only for IDEs? A: No. IDEs are popular clients, but any assistant, agent, or voice bot can speak Model Context Protocol (MCP) to reach your tools.
Q: Will this Python server work with real MCP clients? A: It follows a simple JSON-over-stdio pattern that many clients support; you can adapt transports and message shapes as needed while keeping the same tools, policies, and audits.
Q: How do I make Model Context Protocol (MCP) safe for production? A: Keep read-only defaults, strict allow-lists, short timeouts, rate limits, full audits, and approvals for risky actions. Expand with OAuth, mTLS, and signed releases.
Q: Why does keyword density matter here? A: Because developers are actively searching for Model Context Protocol (MCP) tutorials, security guides, and Python examples. Clear repetition helps search engines understand the topic and helps readers find exactly this guide.
Conclusion
Model Context Protocol (MCP) turns integration chaos into a clean, universal pattern. With one Model Context Protocol (MCP) server, many AI clients can securely access your tools. With one security playbook, your tools stay safe, auditable, and scalable. Ship this Python server, plug it into your stack, and enjoy the feeling of USB-C-style simplicity โ for AI.
If this helped, share it with your team, bookmark it, and clap so more builders can adopt Model Context Protocol (MCP) the right way.
Let's Connect + Fuel the Next Deep Dive โ๏ธ
I build at the intersection of AI, code, and automation โ and I share hands-on guides from LLMs to real-world apps. If this helped, stay in touch (and consider tipping a coffee to keep these coming):
- ๐ง Email: krtarunsingh@gmail.com
- ๐ผ LinkedIn: Tarun Singh
- ๐ป GitHub: github.com/krtarunsingh
- โ Buy Me a Coffee: https://buymeacoffee.com/krtarunsingh
- ๐บ YouTube: @hkaimaster
๐ If you found value here, clap, share, and leave a comment โ it helps more devs discover practical guides like this.
On-Device AI Is Finally Real โ Build a Copilot+ PC App That Runs 100% Offline On-device AI, explained in plain English โ with a full working project you can run today.
Laptop-Only LLM: Tune Google Gemma 3 in Minutes (Code Inside) A clean, from-scratch walkthrough (with code) to tune a 270M-param LLM on chess โ no cloud required.
Build an AI PDF Search Engine in a Weekend (Python, FAISS, RAG โ Full Code) Turn messy folders of PDFs into a blazing-fast, AI-assisted knowledge base you can actually talk to.
The Next AI Boom: What Comes After AI Agents and Agentic AI? Artificial Intelligence is no longer science fiction. It's a living, breathing force that's already transforming how weโฆ
AI-Powered OCR with Phi-3-Vision-128K: The Future of Document Processing In the fast-evolving world of artificial intelligence, multimodal models are setting new standards for integratingโฆ
RAG Frameworks Explored: LlamaIndex vs. LangChain for Next-Gen LLMs The Rise of Large Language Models (LLMs)
Mastering RAG Chunking Techniques for Enhanced Document Processing Dividing large documents into smaller parts is a crucial yet intricate task that significantly impacts the performanceโฆ
A message from our Founder
Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.
Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? We don't receive any funding, we do this to support the community. โค๏ธ
If you want to show some love, please take a moment to follow me on LinkedIn, TikTok, Instagram. You can also subscribe to our weekly newsletter.
And before you go, don't forget to clap and follow the writer๏ธ!