Model Context Protocol (MCP) — Standardizing AI-Tool Communication
What is Model Context Protocol?
The Model Context Protocol (MCP) is an open-source, client-server protocol launched by Anthropic that standardizes how AI applications (clients) communicate with external data sources, developer tools, and APIs (servers).
Think of MCP as the Language Server Protocol (LSP) for AI.
Before LSP, every programming language needed a separate plugin for every IDE (an integration problem). LSP solved this by creating a single standard interface between IDEs and language analysis tools. Similarly, MCP solves the integration problem between LLMs and data/tools.
Why Do We Need MCP?
As AI agents move from simple chat interfaces to autonomous actors, they need context and execution capability:
- Context Isolation: LLMs do not inherently know about your local codebase, your private database, or your Slack channels.
- Custom Integrations: Without a standard, developers have to write bespoke glue code for every tool or database connection.
- Security Risks: Letting an AI execute code or read files requires a strict boundary with user consent. MCP formalizes these boundaries.
Core Architecture
MCP is built on a Client-Server-Host pattern.
The Roles Defined
- Host Application: The end-user application (like Cursor, Claude Desktop, or your custom app) that wants to integrate AI. It hosts the LLM and coordinates the user experience.
- MCP Client: A component inside the host application that maintains the protocol connections to one or more MCP servers. It translates the LLM's requests (e.g. "I want to search for a file") into protocol messages.
- MCP Server: A lightweight, standalone process or web service that exposes data (Resources), actions (Tools), and templates (Prompts) to the client.
- Data Sources / Services: The underlying files, APIs, databases, or operating system shells that the server abstracts.
The Three Pillars of MCP
An MCP server exposes three main capabilities to an AI client:
| Capability | Read/Write | Description | Example Use Case |
|---|---|---|---|
| Resources | Read-Only | Static or dynamic data the server exposes for the AI to read. Act like web pages or file systems. | Schema of a database table, application logs, or a git diff. |
| Tools | Executable | Functions the AI can run to perform actions or mutate state. Requires user confirmation. | Creating a directory, executing a database query, or sending an email. |
| Prompts | Templates | Pre-defined prompt configurations or workflows that help guide the user's interaction. | A slash command like /explain-bug or a pre-seeded system prompt. |
Protocol Communication & Transports
MCP communication uses the JSON-RPC 2.0 protocol, which supports request/response patterns as well as unidirectional notifications.
There are two primary transport layers:
1. Standard Input/Output (stdio)
Designed for local integrations (e.g., when your IDE runs an MCP server on your local machine).
- The Host application spawns the MCP server as a child process.
- They communicate by writing stringified JSON-RPC messages directly to
stdinandstdout. - Crucial rule: Since
stdoutis used for protocol messages, the server code must never write debug logs tostdout(e.g., viaconsole.log). Doing so corrupts the protocol stream. All logging must go tostderr.
2. Server-Sent Events (SSE)
Designed for remote integrations over network boundaries.
- The server hosts a web server.
- The client establishes a unidirectional HTTP connection to receive events (SSE) from the server.
- The client sends command payloads back to the server using standard HTTP POST requests.
Hands-On Example: Custom MCP Server in TypeScript
Let's build a local MCP Server that monitors directory metrics and checks system resources.
Step 3 — Project Initialization
Create a new directory and install dependencies:
mkdir system-mcp-server
cd system-mcp-server
npm init -y
# Install MCP SDK and validation library
npm install @modelcontextprotocol/sdk zod
# Install TypeScript developer dependencies
npm install -D typescript @types/node tsx
npx tsc --initUpdate the tsconfig.json to support ES Modules:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}Step 4 — Implement the Server (src/index.ts)
Create a script file that instantiates the server, registers a resource, and defines a directory analyzer tool.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import fs from "fs";
import os from "os";
import path from "path";
// Initialize the MCP Server
const server = new Server(
{
name: "system-monitor",
version: "1.0.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// ----------------------------------------------------
// 1. RESOURCES: Static or Dynamic Read-only Data
// ----------------------------------------------------
// Expose a list of available resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "system://diagnostics/specs",
name: "Host System Specs",
mimeType: "application/json",
description: "Hardware details of the host operating system.",
},
],
};
});
// Handle reading a specific resource URI
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === "system://diagnostics/specs") {
const specs = {
platform: os.platform(),
architecture: os.arch(),
totalMemoryGB: (os.totalmem() / 1024 / 1024 / 1024).toFixed(2),
freeMemoryGB: (os.freemem() / 1024 / 1024 / 1024).toFixed(2),
cpuCores: os.cpus().length,
cpuModel: os.cpus()[0]?.model || "Unknown",
};
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(specs, null, 2),
},
],
};
}
throw new Error(`Resource not found: ${request.params.uri}`);
});
// ----------------------------------------------------
// 2. TOOLS: Executable Functions (State mutations/Actions)
// ----------------------------------------------------
// Expose available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "analyze_directory",
description:
"Returns file counts and sizes in a specified local directory.",
inputSchema: {
type: "object",
properties: {
directoryPath: {
type: "string",
description: "The absolute path of the directory to analyze.",
},
},
required: ["directoryPath"],
},
},
],
};
});
// Zod schema to validate arguments for "analyze_directory"
const AnalyzeDirectorySchema = z.object({
directoryPath: z.string(),
});
// Handle executing the tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "analyze_directory") {
try {
const parsedArgs = AnalyzeDirectorySchema.parse(args);
const resolvedPath = path.resolve(parsedArgs.directoryPath);
if (!fs.existsSync(resolvedPath)) {
return {
content: [
{
type: "text",
text: `Error: Path does not exist: ${resolvedPath}`,
},
],
isError: true,
};
}
const files = fs.readdirSync(resolvedPath);
let fileCount = 0;
let dirCount = 0;
let totalSize = 0;
for (const file of files) {
const fullPath = path.join(resolvedPath, file);
try {
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
dirCount++;
} else {
fileCount++;
totalSize += stat.size;
}
} catch {
// Skip unreadable files/folders
}
}
const summary = {
path: resolvedPath,
filesFound: fileCount,
foldersFound: dirCount,
totalFileSizeMB: (totalSize / 1024 / 1024).toFixed(2),
};
return {
content: [
{
type: "text",
text: `Directory Analysis Summary:\n${JSON.stringify(summary, null, 2)}`,
},
],
};
} catch (error: any) {
return {
content: [{ type: "text", text: `Failure: ${error.message}` }],
isError: true,
};
}
}
throw new Error(`Tool not found: ${name}`);
});
// ----------------------------------------------------
// 3. SERVER TRANSPORT LIFECYCLE
// ----------------------------------------------------
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
// Note: Standard out is reserved for JSON-RPC. We log strictly to stderr.
console.error("System Monitor MCP Server is running on stdio transport!");
}
main().catch((err) => {
console.error("Fatal error running server:", err);
process.exit(1);
});Step 5 — Adding Script and Building
Update package.json to include an execution command:
"scripts": {
"build": "tsc",
"start": "tsc && node dist/index.js"
}Step 6 — Connect to a Host Application
Configuring in Claude Desktop
To integrate this server into Claude Desktop, open the configuration file:
- macOS/Linux:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add your server config under the mcpServers key:
{
"mcpServers": {
"system-monitor": {
"command": "node",
"args": ["C:/absolute/path/to/system-mcp-server/dist/index.js"]
}
}
}Restart Claude Desktop, and you will see a hammer icon indicating that the resources and tools are now available for Claude to leverage!
Security Model: Humans in the Loop
Security is a primary concern for MCP. The protocol separates data query (Resources) from system mutation (Tools):
Best Practices for Security
- Scope Down Permissions: Don't grant MCP servers full system disk access. Limit them to a specific project directory.
- Input Validation: Never assume the AI will pass valid parameters. Use validation libraries like
zodto validate all incoming arguments insideCallToolRequestSchema. - Explicit Approvals: Ensure the client host app intercepts tool executions and prompts the user before running destructive operations (e.g., executing shell scripts, sending API write actions).
