
Build MCP server đầu tiên bằng TypeScript: tutorial từng bước với Node.js và Claude Desktop
Xây MCP server đầu tiên bằng TypeScript và Node.js theo tutorial từng bước, định nghĩa tool, build project và kết nối với Claude Desktop để test. Nắm
Claude Desktop có thể chat tốt nhưng không tự gọi được API nội bộ của bạn. Đó là lý do MCP (Model Context Protocol) ra đời — chuẩn mở để AI agent kết nối tool và data bên ngoài. Bài này hướng dẫn bạn build một MCP server hoàn chỉnh bằng TypeScript: từ init project Node.js, định nghĩa tool weather lookup gọi API Open-Meteo, connect qua STDIO transport, đến config Claude Desktop để test thực tế. Kết bài có thêm checklist production về bảo mật và version để bạn tránh các lỗi phổ biến khi deploy.
MCP server là gì và vì sao nên tự build
MCP (Model Context Protocol) là chuẩn để AI agent kết nối với tool, dữ liệu và service bên ngoài. Anthropic ra mắt MCP vào tháng 11/2024 và hiện protocol này được Linux Foundation quản lý [F5]. Nói đơn giản: thay vì copy-paste output từ database hay API vào chat, bạn expose chúng qua một MCP server và Claude tự gọi khi cần.
Độ phủ của MCP đã vượt khỏi hệ sinh thái Anthropic. Tính đến tháng 2/2026, registry chính thức ghi nhận hơn 6400 MCP server đã đăng ký [F2]. Theo các nguồn đã thu thập, nhiều provider AI lớn khác cũng đã tích hợp MCP trong năm 2025, đưa nó thành chuẩn chung cho agent tooling. Anthropic phát hành SDK chính thức cho TypeScript, Python, Java, Kotlin, C#, Go, PHP, Ruby, Rust và Swift [F1].
Vậy khi nào nên tự build thay vì cài server có sẵn? Mình thấy 3 trường hợp rõ rệt:
Tool nội bộ — internal admin API, queue worker, billing system không có bản public.
Business logic riêng — workflow đặc thù domain, kết hợp nhiều bước mà server cộng đồng không cover.
Control scope quyền — ví dụ chỉ cho Claude đọc 1 schema cụ thể, không cho write hay xoá.
Một lưu ý trước khi vào code: cộng đồng đã ghi nhận một số lỗ hổng kiến trúc trong code MCP của Anthropic có thể dẫn tới remote code execution và lộ dữ liệu [F3,F4]. Khi bạn tự build server, đặc biệt là server STDIO chạy local, hãy coi việc validate input và giới hạn scope là việc bắt buộc, không phải optional.
Stack bài này mình dùng tối giản: Node.js 20 trở lên, TypeScript, package `@modelcontextprotocol/sdk`, và Claude Desktop làm client để test. Lý do chọn TS: SDK chính thức hỗ trợ TypeScript đầy đủ [F1], type-safe khi define tool schema, và phần lớn dev VN đã quen Node ecosystem. Server cuối bài sẽ chạy được trực tiếp trong Claude Desktop, tổng dưới 80 dòng code.

Setup project: init Node.js + TypeScript trong 2 phút
Tạo folder mới cho project và init Node.js theo cách chuẩn. May mắn là Anthropic đã publish SDK chính thức cho TypeScript nên mình không phải tự implement protocol từ scratch [F1]. Mở terminal trong VSCode hoặc Cursor rồi chạy lần lượt các lệnh dưới đây — toàn bộ setup mất chưa đến 2 phút.
mkdir my-mcp-server && cd my-mcp-server
npm init -y
# Runtime deps
npm install @modelcontextprotocol/sdk zod
# Dev deps
npm install -D typescript @types/node tsxHai dependency chính ở đây có vai trò rõ ràng. `@modelcontextprotocol/sdk` lo phần protocol layer: handshake với client, parse message JSON-RPC, manage transport stdio hoặc HTTP. Còn `zod` dùng để define schema cho tool input — Claude sẽ đọc schema này để biết cần truyền tham số gì khi gọi tool. Phần dev deps gồm `typescript` để compile, `@types/node` cho type definition của Node API, và `tsx` để chạy file TS trực tiếp khi dev mà không cần build mỗi lần.
Tiếp theo là config TypeScript. Mình target ES2022 vì Node 18 trở lên hỗ trợ đầy đủ, dùng `module: Node16` để chạy native ESM (MCP SDK distribute dưới dạng ESM nên phải khớp), và output ra folder `dist`. Bật `strict: true` để TS catch lỗi sớm, đặc biệt quan trọng khi viết tool có nhiều input parameter.
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}Trong `package.json`, nhớ thêm `"type": "module"` để Node treat các file `.js` trong `dist` như ESM. Cấu trúc folder giữ đơn giản: tất cả source code nằm trong `src/`, với `src/index.ts` là entry point. Build sẽ ra `dist/index.js` — đó cũng là file mà Claude Desktop sẽ spawn process khi connect.
{
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js"
}
}Lưu ý cuối trước khi sang phần code: MCP SDK đang ở giai đoạn phát triển nhanh nên API có thể thay đổi giữa các minor version. Mình khuyên pin version chính xác trong `package.json` thay vì để caret range mặc định. Chạy `npm list @modelcontextprotocol/sdk` để xem version đang cài và note lại vào README. Khi gặp breaking change sau này, bạn sẽ biết ngay đang đứng ở đâu để tra changelog.
Định nghĩa tool đầu tiên: weather lookup
Để bắt đầu, mình sẽ chọn một use case đơn giản nhưng rất hữu ích: tra cứu thời tiết. Chúng ta sẽ dùng API miễn phí từ Open-Meteo, không cần đăng ký API key. Điều này giúp bạn tập trung vào cách định nghĩa tool trong MCP mà không bị phân tâm bởi các bước xác thực phức tạp.
Đầu tiên, bạn cần khởi tạo một instance của `McpServer`. Instance này sẽ đại diện cho server MCP của bạn, với một tên và phiên bản cụ thể. Anthropic cung cấp các SDK cho nhiều ngôn ngữ, bao gồm TypeScript, để bạn dễ dàng triển khai [F1].
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';
const server = new McpServer({
name: 'weather-server',
version: '1.0.0',
});
const WeatherInputSchema = z.object({
city: z.string().describe('Tên thành phố cần tra cứu thời tiết'),
units: z.enum(['celsius', 'fahrenheit']).default('celsius').describe('Đơn vị nhiệt độ mong muốn'),
});
server.tool({
name: 'get_current_weather',
description: 'Tra cứu thời tiết hiện tại cho một thành phố cụ thể. Hữu ích khi người dùng hỏi về thời tiết.',
input_schema: WeatherInputSchema,
async handler({ city, units }) {
const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t_weather=true&temperature_unit=${units}`);
const data = await response.json();
// Đây chỉ là ví dụ đơn giản, bạn cần xử lý tọa độ thực tế của `city`
return [
{ type: 'text', text: `Thời tiết hiện tại ở ${city}: ${data.current_weather.temperature}°${units === 'celsius' ? 'C' : 'F'}.` },
{ type: 'text', text: `Tốc độ gió: ${data.current_weather.windspeed} m/s.` }
];
},
});Trong đoạn code trên, mình đã định nghĩa một tool tên là `get_current_weather` bằng phương thức `server.tool()`. Mỗi tool cần có một `name` duy nhất và một `description` rõ ràng. `description` này rất quan trọng vì Claude sẽ dựa vào đó để quyết định khi nào nên gọi tool của bạn.
Tiếp theo là `input_schema`, nơi bạn định nghĩa các tham số mà tool này mong đợi. Mình sử dụng thư viện `zod` để tạo schema, giúp đảm bảo dữ liệu đầu vào luôn hợp lệ. Ở đây, tool cần `city` (kiểu string) và `units` (kiểu enum với 'celsius' hoặc 'fahrenheit'). Việc định nghĩa schema chặt chẽ giúp Claude hiểu rõ cách gọi tool và các tham số cần thiết.
Cuối cùng là `handler`, một hàm async chứa logic thực thi của tool. Trong ví dụ này, `handler` sẽ gọi API của Open-Meteo để lấy thông tin thời tiết. Sau khi nhận được dữ liệu, nó sẽ trả về một mảng các đối tượng content, có thể là văn bản hoặc các loại content khác, để Claude hiển thị cho người dùng. Lưu ý rằng ví dụ này sử dụng tọa độ cố định; trong một ứng dụng thực tế, bạn sẽ cần một cách để chuyển đổi tên thành phố thành tọa độ.
Connect server qua STDIO transport
MCP hỗ trợ hai kiểu transport chính: STDIO (Standard Input/Output) và HTTP/SSE. STDIO thường dùng cho các server chạy cục bộ cùng Claude Desktop, còn HTTP/SSE phù hợp hơn cho các server từ xa. Trong bài này, mình sẽ tập trung vào STDIO vì nó đơn giản và dễ thiết lập cho mục đích phát triển local.
Để kết nối server của bạn qua STDIO, chúng ta cần tạo một thể hiện của `StdioServerTransport` và gọi phương thức `connect()` của server. Đây là phần `main()` cơ bản cho ứng dụng của bạn:
import { StdioServerTransport } from '@anthropic-ai/model-context-protocol';
import { MyCustomServer } from './my-custom-server';
async function main() {
const server = new MyCustomServer(); // Server bạn đã xây dựng
const transport = new StdioServerTransport();
// Kết nối server với transport
await server.connect(transport);
// Xử lý lỗi: KHÔNG log ra stdout để tránh phá vỡ giao thức MCP
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err); // Log ra stderr
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason); // Log ra stderr
process.exit(1);
});
}
main().catch((err) => {
console.error('Fatal error:', err); // Log ra stderr
process.exit(1);
});Một lưu ý quan trọng về xử lý lỗi: bạn không nên in bất kỳ thông tin nào ra `stdout` (Standard Output) khi server đang chạy, vì điều này có thể làm hỏng giao thức MCP. Thay vào đó, hãy sử dụng `stderr` (Standard Error) để ghi log lỗi. Sau khi code xong, bạn có thể build project bằng `npx tsc` để tạo ra các file JavaScript trong thư mục `dist/`.
Trước khi kết nối với Claude Desktop, bạn nên dùng MCP Inspector để kiểm tra nhanh server của mình. Điều này giúp xác minh server hoạt động đúng như mong đợi. Cần lưu ý rằng có một lỗ hổng kiến trúc trong mã MCP của Anthropic, được nhúng trong hầu hết các MCP STDIO cục bộ, có khả năng tạo điều kiện cho các cuộc tấn công chuỗi cung ứng AI trên diện rộng [F3]. Các lỗ hổng này có thể dẫn đến thực thi mã và lộ dữ liệu [F4].

Kết nối Claude Desktop và test thực tế
Server đã build xong, giờ tới bước nối vào Claude Desktop. Bạn mở file `claude_desktop_config.json` — trên macOS path là `~/Library/Application Support/Claude/claude_desktop_config.json`, trên Windows là `%APPDATA%\Claude\claude_desktop_config.json`. Nếu file chưa tồn tại, tạo mới với object rỗng `{}` trước. Đây là nơi Claude Desktop đọc danh sách MCP server lúc khởi động.
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/absolute/path/to/your-project/dist/index.js"]
}
}
}Lưu ý quan trọng: path trong `args` phải là absolute. Dùng relative path sẽ fail vì Claude Desktop spawn child process từ working directory khác chứ không phải thư mục project của bạn. Save config xong, bạn phải quit Claude Desktop hoàn toàn — Cmd+Q trên Mac hoặc tắt từ system tray trên Windows. Đóng cửa sổ chat không đủ, process vẫn còn chạy nền.
Khi Claude Desktop khởi động lại và load thành công, bạn sẽ thấy icon tool (hình búa nhỏ) ở góc dưới input box. Click vào sẽ list ra tool `get_weather` của server. Test bằng prompt: 'Thời tiết Hà Nội hôm nay thế nào?'. Claude tự nhận ra cần gọi tool, hiển thị box xác nhận với input dạng `{"city": "Hanoi"}`, bạn approve, và nó trả lời dựa trên output thật từ API.
Nếu icon tool không xuất hiện, server đã fail load. Bước đầu tiên là check log: trên macOS các file log nằm tại `~/Library/Logs/Claude/`, mỗi server có file `mcp-server-<name>.log` riêng. Log này capture stderr của process, nên error import module hay runtime exception đều show ở đây. Đọc log là cách nhanh nhất để khoanh vùng root cause.
Path relative thay vì absolute → chạy `pwd` trong project rồi paste full path vào `args`
Quên chạy `npm run build` → file `dist/index.js` không tồn tại, log sẽ báo `Cannot find module`
Node version cũ (<18) → upgrade qua `nvm install 20` rồi `nvm use 20`
JSON config sai cú pháp (thiếu dấu phẩy, thừa ngoặc) → validate bằng `jq . claude_desktop_config.json`
Server crash silently khi start → thêm `console.error('boot ok')` vào đầu `index.ts` để confirm process chạy được
Sau khi fix lỗi, lại quit-and-relaunch Claude Desktop để config mới có hiệu lực. Khi tool icon xuất hiện và prompt test trả đúng kết quả, bạn đã có MCP server đầu tiên chạy end-to-end trên máy.
Lưu ý production: bảo mật, version, mở rộng
Khi đưa MCP server vào môi trường production, bạn cần đặc biệt chú ý đến bảo mật và khả năng mở rộng. Một lỗi nhỏ có thể dẫn đến những hậu quả nghiêm trọng, đặc biệt khi các tool chạy với quyền của người dùng.
Đầu tiên, hãy luôn validate chặt chẽ mọi input từ Claude. Các cuộc tấn công prompt injection có thể lợi dụng kẽ hở để kích hoạt các tool không mong muốn hoặc truy cập dữ liệu nhạy cảm [F3,F4]. Tuyệt đối không expose secret như API keys qua output của tool.
Thứ hai, về versioning, hãy pin chặt phiên bản SDK của MCP trong file `package.json` của bạn. Model Context Protocol là một tiêu chuẩn nền tảng được Anthropic ra mắt vào tháng 11 năm 2024 và đang phát triển rất nhanh [F5]. Việc pin version giúp tránh các breaking change không mong muốn.
Khi cần hỗ trợ nhiều người dùng hoặc triển khai server từ xa, bạn nên chuyển từ transport STDIO sang HTTP. Điều này cho phép deploy server độc lập, dễ dàng quản lý và scale hơn. Về khả năng mở rộng, bạn có thể thêm nhiều tool vào cùng một server hoặc tách các server thành nhiều domain riêng biệt tùy theo kiến trúc ứng dụng.
Anthropic cung cấp các SDK tham chiếu cho nhiều ngôn ngữ như TypeScript, Python, Java [F1]. Để tìm hiểu sâu hơn, bạn nên tham khảo tài liệu chính thức tại modelcontextprotocol.io và kho lưu trữ `github.com/modelcontextprotocol/typescript-sdk`.
Tóm lại, build MCP server bằng TypeScript SDK chỉ cần khoảng 50 dòng code cho tool đơn giản, phần lớn thời gian là config Claude Desktop và debug STDIO. Nếu bạn muốn xem flow chạy live và các bước debug chi tiết, tham khảo video gốc hoặc bài viết về 3 MCP server đáng cài đầu tiên trên vibeclaude.net.