Model Context Protocol (MCP) Guide
Table of Contents
- What is MCP?
- Core Concepts
- Architecture
- Getting Started
- Building an MCP Server
- Building an MCP Client
- Transport Mechanisms
- Tools, Resources & Prompts
- Security Considerations
- Debugging & Testing
- Real-World Examples
What is MCP?
Model Context Protocol (MCP) is an open standard developed by Anthropic that enables AI models to securely connect with external data sources, tools, and services. It defines a universal, open protocol for how AI applications communicate with the outside world.
Think of MCP as a USB-C port for AI — a standardized interface that lets any AI host talk to any MCP-compatible server without bespoke integrations.
Why MCP?
Before MCP, every AI application needed custom integrations for each tool or data source. This led to:
- Duplicated effort across teams and organizations
- Inconsistent security and permission models
- Fragile, hard-to-maintain glue code
MCP solves this by providing a single protocol that separates AI hosts (apps running models) from MCP servers (tools and data providers).
Core Concepts
| Concept | Description |
|---|---|
| Host | An AI application (e.g., Claude.ai, an IDE plugin) that connects to MCP servers |
| Client | A component inside the host that manages one connection to one MCP server |
| Server | A lightweight process that exposes tools, resources, or prompts to the host |
| Transport | The communication channel between client and server (stdio, HTTP/SSE) |
| Tool | A function the AI can invoke to take an action (e.g., search the web, write a file) |
| Resource | Read-only data the AI can access (e.g., a file, a database record) |
| Prompt | A reusable prompt template the server exposes to the host |
Architecture
┌─────────────────────────────────────────┐
│ MCP Host │
│ (Claude Desktop, VS Code, your app) │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Client │ │ Client │ ... │
│ └────┬────┘ └────┬────┘ │
└───────┼───────────────┼─────────────────┘
│ │
[Transport] [Transport]
│ │
┌────▼────┐ ┌────▼────┐
│ MCP │ │ MCP │
│ Server │ │ Server │
│ │ │ │
│ (files) │ │ (API) │
└─────────┘ └─────────┘
Each client maintains a 1:1 connection with exactly one server. The host manages multiple clients, allowing the AI to reach multiple servers simultaneously.
Getting Started
Prerequisites
- Node.js 18+ or Python 3.10+
- Basic familiarity with async programming
Installation (TypeScript SDK)
npm install @modelcontextprotocol/sdk
Installation (Python SDK)
pip install mcp
Quickstart: Hello World Server
TypeScript:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "hello-world",
version: "1.0.0",
});
server.tool("greet", { name: z.string() }, async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }],
}));
const transport = new StdioServerTransport();
await server.connect(transport);
Python:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("hello-world")
@mcp.tool()
def greet(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run()
Building an MCP Server
Server Lifecycle
An MCP server goes through three phases:
- Initialization — the client sends
initialize, the server responds with its capabilities - Operation — the server handles requests (tool calls, resource reads, etc.)
- Shutdown — the connection is cleanly closed
Defining Tools
Tools are functions with a name, description, and JSON Schema input. They perform actions and return results.
server.tool(
"read_file",
"Read the contents of a file from disk",
{
path: z.string().describe("Absolute path to the file"),
},
async ({ path }) => {
const content = await fs.readFile(path, "utf-8");
return {
content: [{ type: "text", text: content }],
};
}
);
Defining Resources
Resources expose read-only data. They can be static or dynamic (using URI templates).
// Static resource
server.resource("config://app", "Application config", async () => ({
contents: [{ uri: "config://app", text: JSON.stringify(config) }],
}));
// Dynamic resource via URI template
server.resource(
"file://{path}",
"Read any file",
async (uri) => {
const path = uri.pathname;
const text = await fs.readFile(path, "utf-8");
return { contents: [{ uri: uri.href, text }] };
}
);
Defining Prompts
Prompts are reusable message templates with optional arguments.
server.prompt(
"summarize",
"Summarize a given text",
{ text: z.string() },
({ text }) => ({
messages: [
{
role: "user",
content: { type: "text", text: `Please summarize:\n\n${text}` },
},
],
})
);
Building an MCP Client
A client connects to a server, negotiates capabilities, and makes requests on behalf of the host.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const transport = new StdioClientTransport({
command: "node",
args: ["my-server.js"],
});
const client = new Client({ name: "my-client", version: "1.0.0" });
await client.connect(transport);
// List available tools
const { tools } = await client.listTools();
// Call a tool
const result = await client.callTool({
name: "greet",
arguments: { name: "World" },
});
console.log(result.content[0].text); // "Hello, World!"
Transport Mechanisms
MCP supports two transport types:
1. stdio (Standard I/O)
Best for local servers launched as child processes. The client writes JSON-RPC messages to the server’s stdin and reads from stdout.
const transport = new StdioServerTransport();
Use when: the server runs on the same machine as the host (e.g., a CLI tool, a local database wrapper).
2. HTTP with SSE (Server-Sent Events)
Best for remote or shared servers accessible over the network. The client POSTs requests to an HTTP endpoint; the server pushes responses via SSE.
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
app.get("/sse", (req, res) => {
const transport = new SSEServerTransport("/message", res);
server.connect(transport);
});
Use when: the server is hosted remotely, shared across multiple users, or deployed as a web service.
Tools, Resources & Prompts
Choosing Between Them
| Use | When |
|---|---|
| Tool | The AI needs to take an action or fetch dynamic data (side effects OK) |
| Resource | The AI needs to read stable, structured data without side effects |
| Prompt | You want to offer reusable prompt patterns the AI can invoke by name |
Content Types in Tool Responses
Tool results can return multiple content blocks:
return {
content: [
{ type: "text", text: "Here is the result:" },
{ type: "image", data: base64String, mimeType: "image/png" },
{ type: "resource", resource: { uri: "file:///output.csv", text: csvData } },
],
};
Handling Errors in Tools
Signal tool-level errors without crashing the server by using isError:
try {
const result = await riskyOperation();
return { content: [{ type: "text", text: result }] };
} catch (err) {
return {
isError: true,
content: [{ type: "text", text: `Error: ${err.message}` }],
};
}
Security Considerations
Trust Boundaries
- Hosts must obtain explicit user consent before connecting to any server
- Servers should validate all inputs — never trust the client unconditionally
- Users must approve tool invocations that have side effects
Key Principles
Least Privilege — servers should only request the permissions they genuinely need.
Input Validation — always validate and sanitize inputs before using them in file paths, SQL queries, or shell commands.
Prompt Injection — be cautious when tools read external content (web pages, files) that could contain instructions trying to hijack the AI’s behavior.
Secrets Management — never hardcode credentials in server code. Use environment variables or a secrets manager.
// Good: read from environment
const apiKey = process.env.MY_API_KEY;
if (!apiKey) throw new Error("MY_API_KEY is not set");
Debugging & Testing
Using the MCP Inspector
The MCP Inspector is an interactive browser-based tool for testing servers without a full host.
npx @modelcontextprotocol/inspector node my-server.js
It lets you list tools/resources/prompts, call tools interactively, and inspect raw JSON-RPC messages.
Enabling Debug Logging
TypeScript:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "my-server", version: "1.0.0" });
// All stderr output is safe — it won't corrupt the stdio transport
console.error("[debug] Server started");
Python:
import logging
logging.basicConfig(level=logging.DEBUG)
Writing Unit Tests
Test tool logic in isolation before wiring it into a full server:
import { describe, it, expect } from "vitest";
import { greetUser } from "./tools/greet.js";
describe("greetUser", () => {
it("returns a greeting", async () => {
const result = await greetUser({ name: "Alice" });
expect(result.content[0].text).toBe("Hello, Alice!");
});
});
Real-World Examples
Example 1: Filesystem Server
Expose local files as resources and provide tools to read/write them.
from mcp.server.fastmcp import FastMCP
from pathlib import Path
mcp = FastMCP("filesystem")
ROOT = Path("/home/user/documents")
@mcp.resource("file://{filename}")
def read_file(filename: str) -> str:
"""Read a file from the documents folder."""
return (ROOT / filename).read_text()
@mcp.tool()
def write_file(filename: str, content: str) -> str:
"""Write content to a file in the documents folder."""
(ROOT / filename).write_text(content)
return f"Written {len(content)} bytes to {filename}"
Example 2: REST API Wrapper
Wrap an external REST API as MCP tools so the AI can interact with it.
server.tool(
"search_products",
"Search the product catalog",
{ query: z.string(), limit: z.number().default(10) },
async ({ query, limit }) => {
const res = await fetch(
`https://api.example.com/products?q=${encodeURIComponent(query)}&limit=${limit}`,
{ headers: { Authorization: `Bearer ${process.env.API_KEY}` } }
);
const data = await res.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
Example 3: Database Query Tool
import sqlite3
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("sqlite-server")
DB_PATH = "data.db"
@mcp.tool()
def run_query(sql: str) -> str:
"""Run a read-only SQL query against the database."""
if not sql.strip().upper().startswith("SELECT"):
raise ValueError("Only SELECT queries are allowed")
conn = sqlite3.connect(DB_PATH)
cursor = conn.execute(sql)
rows = cursor.fetchall()
cols = [d[0] for d in cursor.description]
conn.close()
return "\n".join([",".join(cols)] + [",".join(map(str, r)) for r in rows])
Further Reading
- Official MCP Documentation
- MCP Specification
- TypeScript SDK on GitHub
- Python SDK on GitHub
- MCP Server Registry
Guide covers MCP as of early 2025. The protocol is actively evolving — always check the official spec for the latest changes.