MCP (Model Context Protocol) Complete Guide | AI Agent Standard Protocol
이 글의 핵심
MCP (Model Context Protocol) is an open protocol that allows AI models to communicate with external resources — file systems, databases, web APIs — in a standardized way. Supported by Claude Desktop, Cursor, VS Code, and more.
What is MCP?
MCP (Model Context Protocol) is an open-source protocol Anthropic published in late 2024. It lets AI models communicate with external resources — file systems, databases, web APIs, dev tools — in a standardized way.
Client (Claude Desktop, Cursor, VS Code)
↕ MCP Protocol
Server (file system, DB, web API, Git, Slack...)
Why MCP?
Before MCP, every AI app had to write its own integration code for every external tool. MCP provides a USB-like standard interface: write an MCP server once and reuse it across all compatible clients.
| Before MCP | With MCP |
|---|---|
| Each app writes its own API integration | Write MCP server once → all clients use it |
| Model changes require code rewrites | Works regardless of model or client |
| Security/auth reimplemented repeatedly | Handled consistently on the server side |
MCP Architecture
┌─────────────────────────────────────────┐
│ MCP Client │
│ (Claude Desktop / Cursor / VS Code) │
└────────────────┬────────────────────────┘
│ JSON-RPC over stdio / SSE
┌────────────────▼────────────────────────┐
│ MCP Server │
│ ┌──────────┐ ┌──────────┐ ┌───────┐ │
│ │ Tools │ │Resources │ │Prompts│ │
│ └──────────┘ └──────────┘ └───────┘ │
└────────────────┬────────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
File system Database Web API
3 Core Concepts
- Tools: Functions the AI can call (similar to Function Calling)
- Resources: Data the AI can read (files, DB records, etc.)
- Prompts: Reusable prompt templates
Quick Start: Python MCP Server
Installation
pip install mcp
# or using uv (recommended)
pip install uv
uv add mcp
Your First MCP Server
The list_tools function is implemented below. It handles the core logic described above:
# server.py
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import mcp.types as types
# Create server instance
server = Server("my-first-server")
@server.list_tools()
async def list_tools() -> list[Tool]:
"""Return list of available tools"""
return [
Tool(
name="calculate",
description="Evaluates a mathematical expression",
inputSchema={
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Expression to evaluate (e.g. 2 + 3 * 4)"
}
},
"required": ["expression"]
}
),
Tool(
name="get_current_time",
description="Returns the current time",
inputSchema={
"type": "object",
"properties": {}
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle tool calls"""
if name == "calculate":
expression = arguments.get("expression", "")
try:
# Safe eval (use a proper sandbox in production)
result = eval(expression, {"__builtins__": {}}, {})
return [TextContent(type="text", text=f"{expression} = {result}")]
except Exception as e:
return [TextContent(type="text", text=f"Error: {str(e)}")]
elif name == "get_current_time":
from datetime import datetime
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return [TextContent(type="text", text=f"Current time: {now}")]
raise ValueError(f"Unknown tool: {name}")
async def main():
# Run in stdio mode (for local Claude Desktop connections)
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="my-first-server",
server_version="1.0.0",
capabilities=server.get_capabilities(
notification_options=None,
experimental_capabilities={}
)
)
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Real-World Example: File System MCP Server
The list_tools function is implemented below. It handles the core logic described above:
# file_server.py
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from pathlib import Path
import os
server = Server("file-system-server")
ALLOWED_DIR = Path(os.environ.get("ALLOWED_DIR", "/tmp/mcp-files"))
ALLOWED_DIR.mkdir(exist_ok=True)
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="read_file",
description="Read file contents",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string", "description": "File path"}
},
"required": ["path"]
}
),
Tool(
name="write_file",
description="Write content to a file",
inputSchema={
"type": "object",
"properties": {
"path": {"type": "string", "description": "File path"},
"content": {"type": "string", "description": "Content to write"}
},
"required": ["path", "content"]
}
),
Tool(
name="list_files",
description="List files in a directory",
inputSchema={
"type": "object",
"properties": {
"directory": {"type": "string", "description": "Directory path (defaults to base dir)"}
}
}
)
]
def validate_path(path_str: str) -> Path:
"""Security: only allow paths within the permitted directory"""
path = (ALLOWED_DIR / path_str).resolve()
if not str(path).startswith(str(ALLOWED_DIR)):
raise ValueError("Access denied: path is outside the allowed directory")
return path
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "read_file":
path = validate_path(arguments["path"])
if not path.exists():
return [TextContent(type="text", text=f"Error: file not found: {path}")]
content = path.read_text(encoding="utf-8")
return [TextContent(type="text", text=content)]
elif name == "write_file":
path = validate_path(arguments["path"])
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(arguments["content"], encoding="utf-8")
return [TextContent(type="text", text=f"File saved: {path.name}")]
elif name == "list_files":
dir_path = validate_path(arguments.get("directory", "."))
if not dir_path.is_dir():
return [TextContent(type="text", text="Not a directory")]
files = []
for item in sorted(dir_path.iterdir()):
if item.is_dir():
files.append(f"📁 {item.name}/")
else:
size = item.stat().st_size
files.append(f"📄 {item.name} ({size:,} bytes)")
result = "\n".join(files) if files else "No files found"
return [TextContent(type="text", text=result)]
raise ValueError(f"Unknown tool: {name}")
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream,
InitializationOptions(server_name="file-system-server", server_version="1.0.0",
capabilities=server.get_capabilities(notification_options=None, experimental_capabilities={})))
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Building an MCP Server with TypeScript
npm install @modelcontextprotocol/sdk
Import the required modules and set up the dependencies:
// server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fetch from "node-fetch";
const server = new Server(
{ name: "weather-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// Define tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_weather",
description: "Get current weather for a city",
inputSchema: {
type: "object",
properties: {
city: {
type: "string",
description: "City name (in English)",
},
},
required: ["city"],
},
},
],
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "get_weather") {
const city = args?.city as string;
const apiKey = process.env.OPENWEATHER_API_KEY;
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
);
const data = (await response.json()) as any;
if (data.cod !== 200) {
return {
content: [{ type: "text", text: `Error: ${data.message}` }],
};
}
return {
content: [
{
type: "text",
text: `${city} weather: ${data.weather[0].description}, ${data.main.temp}°C (feels like ${data.main.feels_like}°C)`,
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
Connecting to Claude Desktop
Configuration file:
// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
// %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
"mcpServers": {
"my-calculator": {
"command": "python",
"args": ["/path/to/server.py"],
"env": {}
},
"file-system": {
"command": "python",
"args": ["/path/to/file_server.py"],
"env": {
"ALLOWED_DIR": "/Users/username/Documents"
}
},
"weather": {
"command": "node",
"args": ["/path/to/weather-server/build/index.js"],
"env": {
"OPENWEATHER_API_KEY": "your-api-key-here"
}
}
}
}
Restart Claude Desktop and a 🔧 tool icon will appear at the bottom of the chat window.
Using Resources and Prompts
Resources (Reading Data)
The list_resources function is implemented below. It handles the core logic described above:
from mcp.types import Resource, TextResourceContents
@server.list_resources()
async def list_resources() -> list[Resource]:
return [
Resource(
uri="file:///config.json",
name="Config file",
description="Application configuration",
mimeType="application/json"
)
]
@server.read_resource()
async def read_resource(uri: str) -> str | bytes:
if uri == "file:///config.json":
return '{"version": "1.0", "debug": false}'
raise ValueError(f"Unknown resource: {uri}")
Prompts (Reusable Templates)
The list_prompts function is implemented below. It handles the core logic described above:
from mcp.types import Prompt, PromptArgument, GetPromptResult, PromptMessage
@server.list_prompts()
async def list_prompts() -> list[Prompt]:
return [
Prompt(
name="code_review",
description="Code review request prompt",
arguments=[
PromptArgument(name="language", description="Programming language", required=True),
PromptArgument(name="focus", description="Review focus area", required=False)
]
)
]
@server.get_prompt()
async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult:
if name == "code_review":
language = arguments.get("language", "code") if arguments else "code"
focus = arguments.get("focus", "overall quality") if arguments else "overall quality"
return GetPromptResult(
description=f"{language} code review",
messages=[
PromptMessage(
role="user",
content=TextContent(
type="text",
text=f"""Please review the following {language} code with a focus on {focus}:
1. Code quality and readability
2. Potential bugs and errors
3. Performance optimization opportunities
4. Security vulnerabilities
5. Improvement suggestions (with code examples)"""
)
)
]
)
raise ValueError(f"Unknown prompt: {name}")
Popular MCP Servers
Official MCP servers from Anthropic and the community:
Run the following commands:
# Filesystem server (official)
npx -y @modelcontextprotocol/server-filesystem /path/to/directory
# SQLite server (official)
npx -y @modelcontextprotocol/server-sqlite /path/to/database.db
# GitHub server (official)
npx -y @modelcontextprotocol/server-github
# PostgreSQL server
npx -y @modelcontextprotocol/server-postgres postgresql://user:pass@localhost/db
# Slack server
npx -y @modelcontextprotocol/server-slack
# Brave Search server
npx -y @modelcontextprotocol/server-brave-search
Conclusion
MCP is rapidly becoming core infrastructure for the AI agent ecosystem. Learning it now gives you a significant edge in AI agent development.
Next steps: