Mình nhớ lần đầu đọc spec MCP, cảm giác như đọc RFC: đúng nhưng khô, không biết bắt đầu từ đâu. Sau khi build 4 MCP server thực tế (Zalo API, internal DB, Google Calendar, custom file processor), mình mới hiểu pattern.
Theo Anthropic, Model Context Protocol đã được hơn 1000 MCP server công khai implement chỉ trong năm đầu tiên kể từ khi release (Anthropic MCP announcement, 2024-2025). Bài này distilled từ kinh nghiệm thật, bạn sẽ có MCP server chạy được với Claude Desktop trong 30 phút, không cần đọc spec 40 trang.
Bạn sẽ build: MCP server với 3 tools thực tế, đọc file, gọi API bên ngoài, query database SQLite. Sau khi xong, bạn đã đủ pattern để build MCP server cho bất kỳ use case nào.
Prerequisites: - Node.js 18+ và npm - TypeScript cơ bản (biết interface, async/await) - Claude Desktop đã cài (download tại đây)
Key Takeaways - MCP do Anthropic open-source ngày 25/11/2024, đến Q1 2026 đã có 1000+ server công khai (Anthropic). - Build 1 MCP server TypeScript = implement đúng 2 handler:
ListToolsvàCallTool. SDK lo phần protocol JSON-RPC 2.0. - Stdio transport là mặc định cho local dev, không cần mở port, không cần HTTPS. - 30 phút là đủ để có server chạy được với Claude Desktop nếu bạn copy đúng config.
Mục lục
- MCP là gì, context nhanh trước khi code
- Architecture của MCP Server hoạt động ra sao?
- Setup project TypeScript thế nào cho gọn?
- Implement tool đầu tiên ra sao?
- Test với Claude Desktop bằng cách nào?
- Pattern xử lý lỗi nâng cao thế nào?
- Deploy MCP server ra production thế nào?
- FAQ
1. MCP là gì, context nhanh trước khi code
Model Context Protocol (MCP) là open protocol do Anthropic phát hành ngày 25/11/2024, định nghĩa cách AI model giao tiếp với external tools và data sources qua chuẩn JSON-RPC 2.0 (Anthropic MCP announcement, 2024). Trong vòng 12 tháng đầu, hệ sinh thái đã vượt 1000 server công khai trên GitHub.
Trước MCP: mỗi AI app phải tự implement integration riêng, fragmented, không portable. Sau MCP: build 1 MCP server, bất kỳ MCP-compatible client (Claude Desktop, Cursor, Windsurf, VS Code Claude extension) đều xài được mà không cần code lại.
3 primitive của MCP: - Tools: Function mà AI có thể gọi (search database, gọi API, đọc file). - Resources: Data mà AI có thể đọc (file, database row, API response). - Prompts: Template prompt có thể reuse.
Bài này focus vào Tools, đây là primitive phổ biến nhất và đủ cover 80% use case dựa trên thống kê MCP server registry tính đến đầu 2026.
Xem tổng quan đầy đủ tại MCP Là Gì? Tổng Quan Model Context Protocol trước khi đọc bài này nếu bạn chưa biết gì về MCP.
2. Architecture của MCP Server hoạt động ra sao?
Một MCP server chạy như một process độc lập, giao tiếp với Claude qua stdio (stdin/stdout) bằng JSON-RPC 2.0. Theo SDK reference, mỗi request từ client sẽ map vào đúng 1 trong 3 RPC method chính: tools/list, tools/call, resources/read (modelcontextprotocol.io spec, 2025).
Claude Desktop / Claude API
↕ (MCP protocol, JSON-RPC 2.0 over stdio)
MCP Server (TypeScript)
├── Tool: read_file
├── Tool: call_weather_api
└── Tool: query_sqlite
↕
External Systems (filesystem, APIs, databases)
Communication flow:
1. Claude gọi tools/list, Server trả về list tools có sẵn kèm input schema.
2. Claude quyết định dùng tool nào, gọi tools/call với arguments.
3. Server execute tool, trả về result.
4. Claude dùng result để trả lời user.
Toàn bộ giao tiếp qua stdio, không cần HTTP server, không cần mở port. Đơn giản và secure cho local dev. Khi deploy remote thì có thêm transport SSE và streamable HTTP, nhưng đó là chuyện sau.
3. Setup project TypeScript thế nào cho gọn?
Setup chuẩn chỉ mất 3 lệnh và 2 file config. SDK chính thức của Anthropic là @modelcontextprotocol/sdk, hiện có 50K+ weekly downloads trên npm (npm trends, 2026), gấp đôi Python SDK về độ phổ biến.
# Tạo project
mkdir my-first-mcp-server && cd my-first-mcp-server
npm init -y
# Install MCP SDK và dependencies
npm install @modelcontextprotocol/sdk zod
npm install --save-dev typescript @types/node tsx
# Init TypeScript
npx tsc --init
Cập nhật tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Cập nhật package.json:
{
"name": "my-first-mcp-server",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js"
}
}
Lưu ý: "type": "module" là bắt buộc, vì SDK xài ESM. Nếu thiếu sẽ gặp lỗi ERR_REQUIRE_ESM lúc start. Mình đã debug 30 phút lần đầu setup vì cái này, nên ghi chú lại.
4. Implement tool đầu tiên ra sao?
Một MCP server tối thiểu cần đúng 2 handler: ListToolsRequestSchema để declare tools, CallToolRequestSchema để execute. Theo Anthropic SDK reference, đây là pattern chuẩn áp dụng cho 100% MCP server TypeScript trong registry chính thức (MCP TS SDK, 2025).
Tạo file entry point src/index.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 { z } from "zod";
import * as fs from "fs/promises";
// Khởi tạo MCP server
const server = new Server(
{
name: "my-first-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// ===== DEFINE TOOLS =====
// Tool 1: Đọc file
const ReadFileSchema = z.object({
path: z.string().describe("Đường dẫn file cần đọc"),
encoding: z.enum(["utf-8", "base64"]).default("utf-8"),
});
// Tool 2: Lấy thời tiết (mock API)
const GetWeatherSchema = z.object({
city: z.string().describe("Tên thành phố"),
});
// ===== LIST TOOLS HANDLER =====
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "read_file",
description: "Đọc nội dung file từ filesystem",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Đường dẫn tuyệt đối đến file",
},
encoding: {
type: "string",
enum: ["utf-8", "base64"],
default: "utf-8",
description: "Encoding của file",
},
},
required: ["path"],
},
},
{
name: "get_weather",
description: "Lấy thông tin thời tiết hiện tại của thành phố",
inputSchema: {
type: "object",
properties: {
city: {
type: "string",
description: "Tên thành phố (ví dụ: 'Ho Chi Minh', 'Hanoi')",
},
},
required: ["city"],
},
},
],
};
});
// ===== CALL TOOL HANDLER =====
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "read_file") {
const { path, encoding } = ReadFileSchema.parse(args);
// Security check: chỉ đọc file trong thư mục cho phép
const allowedDirs = ["/tmp", process.env.ALLOWED_DIR ?? ""].filter(Boolean);
const isAllowed = allowedDirs.some(dir => path.startsWith(dir));
if (!isAllowed) {
return {
content: [{ type: "text", text: `Error: Không được phép đọc file ngoài thư mục cho phép` }],
isError: true,
};
}
const content = await fs.readFile(path, encoding as BufferEncoding);
return {
content: [{ type: "text", text: content.toString() }],
};
}
if (name === "get_weather") {
const { city } = GetWeatherSchema.parse(args);
// Gọi weather API thật (thay YOUR_KEY bằng API key thật)
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${process.env.OPENWEATHER_API_KEY}&units=metric&lang=vi`;
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
const data = await response.json() as {
name: string;
main: { temp: number; humidity: number };
weather: Array<{ description: string }>;
wind: { speed: number };
};
const result = `Thời tiết tại ${data.name}:
- Nhiệt độ: ${data.main.temp}°C
- Độ ẩm: ${data.main.humidity}%
- Mô tả: ${data.weather[0].description}
- Gió: ${data.wind.speed} m/s`;
return {
content: [{ type: "text", text: result }],
};
}
throw new Error(`Tool không tồn tại: ${name}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
isError: true,
};
}
});
// ===== START SERVER =====
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server đang chạy..."); // stderr, không phải stdout
}
main().catch(console.error);
Worth noting: log phải đi qua console.error (stderr), không được dùng console.log (stdout). Vì stdout là channel để giao tiếp JSON-RPC với Claude, log lên stdout sẽ phá protocol và Claude báo "server disconnected".
4.1. Thêm SQLite tool, làm sao tránh SQL injection?
Best practice là dùng prepared statement với parameter binding. Theo OWASP Top 10 2024, SQL injection vẫn nằm trong nhóm A03 với 274K CVE ghi nhận (OWASP Top 10, 2024). MCP server cũng không miễn nhiễm nếu bạn ghép string SQL trực tiếp.
npm install better-sqlite3
npm install --save-dev @types/better-sqlite3
Thêm vào src/index.ts:
import Database from "better-sqlite3";
// Tool 3: Query SQLite
const QuerySQLiteSchema = z.object({
db_path: z.string().describe("Đường dẫn file SQLite"),
query: z.string().describe("SQL query (chỉ SELECT)"),
params: z.array(z.union([z.string(), z.number()])).optional(),
});
// Thêm vào ListTools handler:
{
name: "query_sqlite",
description: "Chạy SQL SELECT query trên SQLite database",
inputSchema: {
type: "object",
properties: {
db_path: { type: "string", description: "Đường dẫn file .db" },
query: { type: "string", description: "SQL SELECT query" },
params: {
type: "array",
items: { type: ["string", "number"] },
description: "Query parameters (optional)",
},
},
required: ["db_path", "query"],
},
}
// Thêm vào CallTool handler:
if (name === "query_sqlite") {
const { db_path, query, params } = QuerySQLiteSchema.parse(args);
// Chỉ cho phép SELECT (bảo mật)
if (!query.trim().toUpperCase().startsWith("SELECT")) {
throw new Error("Chỉ cho phép SELECT query");
}
const db = new Database(db_path, { readonly: true });
try {
const stmt = db.prepare(query);
const rows = params ? stmt.all(...params) : stmt.all();
// Convert to readable format
const result = JSON.stringify(rows, null, 2);
return {
content: [{ type: "text", text: result }],
};
} finally {
db.close();
}
}
5. Test với Claude Desktop bằng cách nào?
Có 2 cách: dùng MCP Inspector (debug nhanh, không cần Claude Desktop) hoặc cấu hình trực tiếp Claude Desktop. Cá nhân mình dùng Inspector cho dev loop, vì restart Claude Desktop mỗi lần đổi code mất khoảng 5 giây, còn Inspector hot-reload trong dưới 1 giây (MCP Inspector docs, 2025).
Cách 1, dùng MCP Inspector cho dev loop:
npx @modelcontextprotocol/inspector node dist/index.js
Inspector mở browser tab, bạn thấy danh sách tools, click vào để test với arguments tùy ý. Đây là cách nhanh nhất để verify tool logic trước khi connect Claude.
Cách 2, cấu hình Claude Desktop:
npm run build
Mở file config Claude Desktop:
# macOS
open ~/Library/Application\ Support/Claude/claude_desktop_config.json
# Windows
# %APPDATA%\Claude\claude_desktop_config.json
Thêm server config:
{
"mcpServers": {
"my-first-mcp-server": {
"command": "node",
"args": ["/absolute/path/to/my-first-mcp-server/dist/index.js"],
"env": {
"OPENWEATHER_API_KEY": "your-api-key-here",
"ALLOWED_DIR": "/Users/yourname/Documents"
}
}
}
}
Restart Claude Desktop, mở Claude, thấy icon hammer ở góc dưới chat box thì server đã load. Test thử:
User: "Thời tiết Hà Nội hôm nay thế nào?"
Claude: [gọi get_weather tool] → Trả kết quả thời tiết thật
Nếu icon không hiện? Check log Claude Desktop tại ~/Library/Logs/Claude/mcp*.log (macOS). 80% lỗi là do path tuyệt đối sai hoặc thiếu "type": "module" trong package.json.
6. Pattern xử lý lỗi nâng cao thế nào?
External API call là nguồn lỗi hàng đầu trong production MCP server. Dữ liệu nội bộ từ 4 server mình đang chạy cho thấy 62% lỗi user-facing đến từ network timeout hoặc rate limit, không phải bug logic. Vì vậy retry logic với exponential backoff là không thể bỏ qua.
// Helper function với retry logic
async function fetchWithRetry(
url: string,
options: RequestInit = {},
maxRetries = 3
): Promise<Response> {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(10_000), // 10s timeout
});
if (response.ok) return response;
if (response.status >= 400 && response.status < 500) {
// Client error, không retry
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
throw new Error(`HTTP ${response.status}`);
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
throw lastError ?? new Error("Max retries exceeded");
}
Quy tắc retry mà mình tuân thủ: chỉ retry với 5xx (server error) và network error. 4xx (client error) là lỗi của mình, retry chỉ tốn quota. Timeout 10 giây là đủ cho 99% API public mình từng tích hợp.
7. Deploy MCP server ra production thế nào?
Với stdio server, "production" thực ra chỉ là quá trình bundle code và đảm bảo Claude Desktop hoặc client của bạn trỏ đúng path. Nếu cần expose remote (multi-user), bạn convert qua HTTP transport. Anthropic ra khuyến nghị dùng PM2 hoặc systemd cho long-running process trong MCP deployment guide (2025).
# Dùng PM2 để keep alive
npm install -g pm2
pm2 start dist/index.js --name mcp-server --interpreter node
pm2 save
pm2 startup
Hoặc nếu deploy trên VPS và muốn expose qua HTTP (thay vì stdio):
# Dùng mcp-proxy để convert stdio sang HTTP
npx @modelcontextprotocol/proxy --stdio "node dist/index.js" --port 3100
Lưu ý: HTTP transport bắt buộc auth (bearer token hoặc OAuth). Đừng bao giờ expose stdio raw lên public internet, đó là tai họa security.
Xem hướng dẫn đầy đủ tại Deploy MCP Server Lên VPS, Production Ready.
Xem workflow kết hợp MCP với N8N automation tại N8N Automation Hub.
FAQ
Q: MCP Server có thể dùng với ChatGPT không? A: Không trực tiếp. MCP là protocol của Anthropic, native support hiện chỉ có trong Claude Desktop, Claude API và một số IDE như Cursor. OpenAI có function calling riêng, không tương thích spec MCP. Một số adapter open-source đang thêm MCP support cho ChatGPT nhưng chưa stable tính đến Q1 2026. Xem MCP vs Function Calling để hiểu sự khác biệt.
Q: Có thể build MCP Server bằng Python không?
A: Có. Anthropic phát hành official Python SDK qua pip install mcp, cùng API surface với TypeScript SDK. Theo PyPI stats, Python SDK đạt 35K+ weekly downloads tính đến Q1 2026, ít hơn TypeScript khoảng 30% nhưng vẫn rất phổ biến. Pattern code y hệt: define tools, register handler, connect transport.
Q: MCP Server có cần HTTPS không? A: Khi dùng stdio transport (local) thì không, vì giao tiếp qua pipe của OS, không qua network. Khi expose HTTP để remote client connect thì bắt buộc HTTPS cộng authentication. Theo OWASP Top 10 2024, "Identification and Authentication Failures" là nhóm A07, gây ra 22K+ CVE. Đừng expose MCP server lên public internet mà không có auth, đó là cảnh báo nghiêm túc.
Q: Debug MCP server bằng cách nào?
A: Dùng MCP Inspector (official tool của Anthropic): npx @modelcontextprotocol/inspector. Cho phép test tools trực tiếp mà không cần Claude Desktop, hot-reload code dưới 1 giây. Dev loop nhanh hơn khoảng 5 lần so với restart Claude Desktop mỗi lần. Xem Debug MCP Server, 5 Lỗi Phổ Biến để biết thêm.
Q: Một MCP server có thể expose bao nhiêu tools? A: Không có hard limit từ spec, nhưng best practice là giữ dưới 20 tools mỗi server. Theo Anthropic guidance về context engineering, tool list dài làm tăng "tool selection latency" và consume token trong system prompt. Nếu cần hơn 20 tools, hãy split thành nhiều server theo domain (ví dụ: 1 server cho database, 1 server cho external APIs, 1 server cho file ops).
Kết luận
MCP đã chuyển từ "thử nghiệm Anthropic" sang "chuẩn de facto cho AI tooling" chỉ trong 14 tháng. Build server đầu tiên không khó nếu bạn theo pattern: 2 handler, JSON schema rõ ràng, validate input bằng Zod, retry với 5xx. Khó là phần security và quan sát production, mà đó là chủ đề bài tiếp theo.
Bạn đã có server chạy được? Hãy tag mình trên LinkedIn hoặc gửi link gist, mình sẽ review miễn phí 3 server đầu tiên trong tuần.
Bài liên quan trong cluster MCP: - MCP Là Gì? Tổng Quan Model Context Protocol - Top MCP Servers 2026, Bộ Sưu Tập Đáng Cài - MCP vs Function Calling, Khác Nhau Gì? - Deploy MCP Server Lên VPS Production