Trung cấpHướng dẫnClaude Code

Xây dựng MCP Server đầu tiên với TypeScript — Hướng dẫn từng bước

Nghe bài viết
00:00

Điểm nổi bật

Nhấn để đến mục tương ứng

  1. 1 Model Context Protocol (MCP) là giao thức mở cho phép các ứng dụng AI như Claude kết nối với dữ liệu và công cụ bên ngoài một cách chuẩn hóa.
  2. 2 Ví dụ thực tế: MCP Server dữ liệu chứng khoán Việt Nam Hãy xây dựng một MCP Server thực tế cung cấp dữ liệu chứng khoán Việt Nam — VN-Index, HNX-Index và thông tin cổ phiếu.
  3. 3 Thay vì viết integration riêng cho từng ứng dụng, bạn xây dựng một MCP Server và mọi client tương thích đều có thể sử dụng.
  4. 4 Định nghĩa Resource đầu tiên: Database Query Resource trong MCP cung cấp dữ liệu có cấu trúc mà AI có thể đọc — tương tự REST endpoint nhưng dành cho AI.
  5. 5 docker run -i my-mcp-server Flag -i (interactive) quan trọng vì MCP Server giao tiếp qua stdin/stdout.
teal and black typewriter

Model Context Protocol (MCP) là giao thức mở cho phép các ứng dụng AI như Claude kết nối với dữ liệu và công cụ bên ngoài một cách chuẩn hóa. Thay vì viết integration riêng cho từng ứng dụng, bạn xây dựng một MCP Server và mọi client tương thích đều có thể sử dụng. Trong bài hướng dẫn này, chúng ta sẽ xây dựng MCP Server đầu tiên với TypeScript — từ thiết lập dự án đến triển khai thực tế.

1. Điều kiện tiên quyết

Trước khi bắt đầu, hãy đảm bảo bạn đã cài đặt:

  • Node.js 18+ — MCP SDK yêu cầu tối thiểu Node.js 18. Kiểm tra bằng node --version
  • TypeScript 5.0+ — Cài đặt toàn cục qua npm install -g typescript
  • Claude Code — Để test MCP Server trực tiếp. Cài đặt qua npm install -g @anthropic-ai/claude-code
  • Kiến thức cơ bản về TypeScript — Hiểu type, interface, async/await

MCP hoạt động theo mô hình client-server: Claude Code (hoặc Claude Desktop) là client, còn server của bạn cung cấp tools (hàm thực thi), resources (dữ liệu đọc) và prompts (template). Giao tiếp qua stdio hoặc HTTP với Server-Sent Events.

2. Thiết lập dự án

Tạo thư mục dự án và khởi tạo:

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

Tạo file tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}

Cập nhật package.json:

{
  "type": "module",
  "bin": {
    "my-mcp-server": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsc --watch"
  }
}

Tạo file entry point src/index.ts:

#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "my-mcp-server",
  version: "1.0.0",
  description: "MCP Server đầu tiên của tôi"
});

// Tools và resources sẽ được đăng ký ở đây

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Server đang chạy trên stdio");
}

main().catch(console.error);

Lưu ý: dùng console.error thay vì console.log vì stdio transport sử dụng stdout cho giao tiếp JSON-RPC. Mọi log phải ghi vào stderr.

3. Định nghĩa Tool đầu tiên: Weather Lookup

Tool trong MCP là hàm mà AI có thể gọi để thực hiện hành động. Hãy tạo một tool tra cứu thời tiết đơn giản:

import { z } from "zod";

server.tool(
  "get_weather",
  "Tra cứu thời tiết hiện tại cho một thành phố",
  {
    city: z.string().describe("Tên thành phố, ví dụ: Hanoi, Ho Chi Minh City"),
    unit: z.enum(["celsius", "fahrenheit"]).default("celsius")
      .describe("Đơn vị nhiệt độ")
  },
  async ({ city, unit }) => {
    // Trong thực tế, gọi API thời tiết ở đây
    const weatherData: Record<string, { temp: number; condition: string }> = {
      "hanoi": { temp: 28, condition: "Nhiều mây" },
      "ho chi minh city": { temp: 33, condition: "Nắng nóng" },
      "da nang": { temp: 30, condition: "Có mưa rào" }
    };

    const key = city.toLowerCase();
    const data = weatherData[key];

    if (!data) {
      return {
        content: [{ type: "text", text: `Không tìm thấy dữ liệu thời tiết cho "${city}"` }],
        isError: true
      };
    }

    const temp = unit === "fahrenheit"
      ? (data.temp * 9/5 + 32).toFixed(1) + "°F"
      : data.temp + "°C";

    return {
      content: [{
        type: "text",
        text: `Thời tiết tại ${city}: ${temp}, ${data.condition}`
      }]
    };
  }
);

Cấu trúc một tool gồm 4 phần: tên định danh (snake_case), mô tả ngắn gọn cho AI hiểu khi nào nên dùng, schema tham số định nghĩa bằng Zod, và hàm xử lý trả về kết quả. Schema Zod được tự động chuyển thành JSON Schema để AI biết cần truyền gì.

4. Định nghĩa Resource đầu tiên: Database Query

Resource trong MCP cung cấp dữ liệu có cấu trúc mà AI có thể đọc — tương tự REST endpoint nhưng dành cho AI. Tạo resource truy vấn danh sách sản phẩm:

server.resource(
  "products",
  "products://list",
  {
    description: "Danh sách sản phẩm trong cơ sở dữ liệu",
    mimeType: "application/json"
  },
  async (uri) => {
    // Trong thực tế, truy vấn database ở đây
    const products = [
      { id: 1, name: "Cà phê Robusta", price: 85000, stock: 150 },
      { id: 2, name: "Cà phê Arabica", price: 120000, stock: 80 },
      { id: 3, name: "Trà Ô Long", price: 95000, stock: 200 }
    ];

    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify(products, null, 2)
      }]
    };
  }
);

Resource có thể sử dụng URI template để hỗ trợ tham số động:

server.resource(
  "product-detail",
  "products://{id}",
  { description: "Chi tiết một sản phẩm theo ID" },
  async (uri, { id }) => {
    // Truy vấn sản phẩm theo id
    const product = await fetchProductById(id);
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify(product, null, 2)
      }]
    };
  }
);

Điểm khác biệt chính: tool dùng để thực hiện hành động (gửi email, tạo bản ghi, gọi API), resource dùng để đọc dữ liệu (lấy cấu hình, danh sách, trạng thái). AI sẽ tự quyết định dùng tool hay resource dựa trên ngữ cảnh câu hỏi.

5. Xử lý Tool Call với Input Validation

Zod tự động validate input trước khi hàm xử lý được gọi. Tuy nhiên, bạn nên thêm business logic validation:

server.tool(
  "create_order",
  "Tạo đơn hàng mới",
  {
    customer_name: z.string().min(2).max(100)
      .describe("Tên khách hàng"),
    items: z.array(z.object({
      product_id: z.number().int().positive(),
      quantity: z.number().int().min(1).max(999)
    })).min(1).max(50)
      .describe("Danh sách sản phẩm trong đơn hàng"),
    shipping_address: z.string().min(10)
      .describe("Địa chỉ giao hàng đầy đủ"),
    note: z.string().optional()
      .describe("Ghi chú đơn hàng")
  },
  async ({ customer_name, items, shipping_address, note }) => {
    // Business validation
    const invalidItems: number[] = [];
    for (const item of items) {
      const product = await getProduct(item.product_id);
      if (!product) {
        invalidItems.push(item.product_id);
      } else if (product.stock < item.quantity) {
        return {
          content: [{
            type: "text",
            text: `Sản phẩm "${product.name}" chỉ còn ${product.stock} trong kho, không đủ ${item.quantity} đơn vị.`
          }],
          isError: true
        };
      }
    }

    if (invalidItems.length > 0) {
      return {
        content: [{
          type: "text",
          text: `Không tìm thấy sản phẩm với ID: ${invalidItems.join(", ")}`
        }],
        isError: true
      };
    }

    // Tạo đơn hàng
    const order = await createOrderInDB({
      customer_name, items, shipping_address, note
    });

    return {
      content: [{
        type: "text",
        text: `Đơn hàng #${order.id} đã được tạo thành công cho ${customer_name}. Tổng giá trị: ${order.total.toLocaleString("vi-VN")}đ`
      }]
    };
  }
);

Một số best practice cho validation: sử dụng Zod schema chi tiết với .min(), .max(), .regex(); luôn thêm .describe() để AI hiểu mục đích từng tham số; trả về isError: true khi có lỗi business logic thay vì throw exception; và giữ thông báo lỗi rõ ràng, cụ thể để AI có thể thử lại hoặc thông báo cho người dùng.

6. Testing với Claude Code

Sau khi code xong, build và test trực tiếp với Claude Code:

# Build TypeScript
npm run build

# Test nhanh với MCP Inspector (công cụ debug chính thức)
npx @modelcontextprotocol/inspector node dist/index.js

MCP Inspector mở giao diện web tại http://localhost:5173, cho phép bạn xem danh sách tools/resources, gọi thử từng tool với tham số tùy chỉnh và kiểm tra response format.

Để test thực tế với Claude Code, thêm server vào cấu hình:

# Thêm MCP server vào Claude Code (scope project)
claude mcp add my-server node /đường-dẫn-tuyệt-đối/dist/index.js

# Hoặc thêm vào scope global
claude mcp add --scope user my-server node /đường-dẫn-tuyệt-đối/dist/index.js

Sau khi thêm, khởi động Claude Code và thử:

# Trong Claude Code, gõ:
Thời tiết Hà Nội hôm nay thế nào?
# Claude sẽ tự động gọi tool get_weather với city="Hanoi"

# Kiểm tra resource:
Cho tôi xem danh sách sản phẩm trong database
# Claude sẽ đọc resource products://list

Kiểm tra danh sách server đã đăng ký:

claude mcp list
# Kết quả:
# my-server: node /path/to/dist/index.js (local)

Nếu cần debug chi tiết, bật log verbose:

CLAUDE_DEBUG=1 claude

7. Xử lý lỗi và Logging

Một MCP Server production cần xử lý lỗi chặt chẽ. Tạo module logging riêng:

// src/logger.ts
import { createWriteStream } from "fs";

const logFile = createWriteStream("mcp-server.log", { flags: "a" });

export function log(level: "info" | "warn" | "error", message: string, data?: unknown) {
  const entry = {
    timestamp: new Date().toISOString(),
    level,
    message,
    ...(data ? { data } : {})
  };
  // Ghi vào file (không dùng stdout vì stdio transport)
  logFile.write(JSON.stringify(entry) + "\n");
  // Cũng ghi vào stderr để debug
  console.error(`[${level.toUpperCase()}] ${message}`);
}

Wrap tool handler với error handling:

function withErrorHandling(
  toolName: string,
  handler: (...args: any[]) => Promise<any>
) {
  return async (...args: any[]) => {
    try {
      log("info", `Tool called: ${toolName}`, { args: args[0] });
      const result = await handler(...args);
      log("info", `Tool completed: ${toolName}`);
      return result;
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      log("error", `Tool failed: ${toolName}`, { error: message });
      return {
        content: [{
          type: "text" as const,
          text: `Lỗi khi thực thi ${toolName}: ${message}`
        }],
        isError: true
      };
    }
  };
}

// Sử dụng:
server.tool(
  "get_weather",
  "Tra cứu thời tiết",
  { city: z.string() },
  withErrorHandling("get_weather", async ({ city }) => {
    // logic ở đây
  })
);

Các loại lỗi cần xử lý: network timeout khi gọi API bên ngoài (đặt timeout hợp lý, thường 10-30 giây); rate limiting (trả về thông báo rõ ràng và thời gian chờ); invalid data từ nguồn bên ngoài (validate response trước khi trả về); và lỗi kết nối database (retry logic với exponential backoff).

8. Triển khai: Docker và npm publish

Đóng gói với Docker

Tạo Dockerfile:

FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
ENTRYPOINT ["node", "dist/index.js"]
# Build và chạy
docker build -t my-mcp-server .
docker run -i my-mcp-server

Flag -i (interactive) quan trọng vì MCP Server giao tiếp qua stdin/stdout. Không cần -t (tty).

Publish lên npm

Cập nhật package.json:

{
  "name": "@your-scope/my-mcp-server",
  "version": "1.0.0",
  "description": "MCP Server cho ...",
  "bin": {
    "my-mcp-server": "./dist/index.js"
  },
  "files": ["dist"],
  "keywords": ["mcp", "mcp-server", "claude"]
}
npm run build
npm publish --access public

Sau khi publish, người dùng cài đặt và đăng ký với Claude Code:

npx @your-scope/my-mcp-server
# Hoặc
claude mcp add my-server npx @your-scope/my-mcp-server

9. Đăng ký trong Claude Code Settings

MCP Server có thể được đăng ký ở nhiều scope khác nhau trong Claude Code:

Project scope (khuyến nghị cho team)

Tạo file .mcp.json tại root dự án:

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["/đường-dẫn/dist/index.js"],
      "env": {
        "API_KEY": "your-api-key"
      }
    }
  }
}

Khi bất kỳ thành viên nào mở Claude Code trong thư mục dự án, server sẽ tự động được nhận diện. File này nên commit vào repo (trừ phần env chứa secret).

User scope (cá nhân)

claude mcp add --scope user my-server node /đường-dẫn/dist/index.js

Server sẽ khả dụng trong mọi dự án bạn mở với Claude Code.

Truyền biến môi trường

claude mcp add my-server -e API_KEY=xxx -e DB_HOST=localhost node dist/index.js

Quản lý server

# Xem danh sách
claude mcp list

# Xem chi tiết
claude mcp get my-server

# Xóa server
claude mcp remove my-server

10. Ví dụ thực tế: MCP Server dữ liệu chứng khoán Việt Nam

Hãy xây dựng một MCP Server thực tế cung cấp dữ liệu chứng khoán Việt Nam — VN-Index, HNX-Index và thông tin cổ phiếu. Server này sử dụng API công khai từ các nguồn dữ liệu chứng khoán Việt Nam.

// src/index.ts
#!/usr/bin/env node
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: "vnstock-mcp",
  version: "1.0.0",
  description: "MCP Server dữ liệu chứng khoán Việt Nam"
});

// Helper: gọi API với timeout và retry
async function fetchWithRetry(url: string, retries = 3): Promise<any> {
  for (let i = 0; i < retries; i++) {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 10000);
      const res = await fetch(url, { signal: controller.signal });
      clearTimeout(timeout);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    } catch (err) {
      if (i === retries - 1) throw err;
      await new Promise(r => setTimeout(r, 1000 * (i + 1)));
    }
  }
}

// Tool 1: Lấy chỉ số thị trường
server.tool(
  "get_market_index",
  "Lấy chỉ số thị trường chứng khoán Việt Nam (VN-Index, HNX-Index, UPCOM)",
  {
    index: z.enum(["VNINDEX", "HNX", "UPCOM"]).default("VNINDEX")
      .describe("Mã chỉ số thị trường")
  },
  async ({ index }) => {
    try {
      const data = await fetchWithRetry(
        `https://api.example.com/market/${index}`
      );
      return {
        content: [{
          type: "text",
          text: [
            `📊 ${index}`,
            `Điểm số: ${data.value.toFixed(2)}`,
            `Thay đổi: ${data.change > 0 ? "+" : ""}${data.change.toFixed(2)} (${data.changePct.toFixed(2)}%)`,
            `Khối lượng: ${(data.volume / 1e6).toFixed(1)} triệu CP`,
            `Giá trị: ${(data.totalValue / 1e9).toFixed(0)} tỷ VNĐ`,
            `Cập nhật: ${data.time}`
          ].join("\n")
        }]
      };
    } catch (error) {
      return {
        content: [{ type: "text", text: `Không thể lấy dữ liệu ${index}. Vui lòng thử lại.` }],
        isError: true
      };
    }
  }
);

// Tool 2: Tra cứu thông tin cổ phiếu
server.tool(
  "get_stock_info",
  "Tra cứu thông tin chi tiết một mã cổ phiếu trên sàn HOSE, HNX hoặc UPCOM",
  {
    symbol: z.string().min(3).max(5).toUpperCase()
      .describe("Mã cổ phiếu, ví dụ: VNM, FPT, VIC, HPG"),
  },
  async ({ symbol }) => {
    try {
      const data = await fetchWithRetry(
        `https://api.example.com/stock/${symbol}`
      );
      return {
        content: [{
          type: "text",
          text: [
            `🏢 ${symbol} — ${data.companyName}`,
            `Sàn: ${data.exchange} | Ngành: ${data.industry}`,
            `Giá hiện tại: ${data.price.toLocaleString("vi-VN")} VNĐ`,
            `Thay đổi: ${data.change > 0 ? "+" : ""}${data.change.toLocaleString("vi-VN")} (${data.changePct.toFixed(2)}%)`,
            `Trần: ${data.ceiling.toLocaleString("vi-VN")} | Sàn: ${data.floor.toLocaleString("vi-VN")} | TC: ${data.ref.toLocaleString("vi-VN")}`,
            `KL khớp lệnh: ${(data.volume / 1e3).toFixed(1)}K`,
            `Vốn hóa: ${(data.marketCap / 1e12).toFixed(2)} nghìn tỷ VNĐ`,
            `P/E: ${data.pe?.toFixed(2) ?? "N/A"} | P/B: ${data.pb?.toFixed(2) ?? "N/A"}`,
          ].join("\n")
        }]
      };
    } catch (error) {
      return {
        content: [{ type: "text", text: `Không tìm thấy mã cổ phiếu "${symbol}". Kiểm tra lại mã và thử lại.` }],
        isError: true
      };
    }
  }
);

// Tool 3: Top cổ phiếu tăng/giảm
server.tool(
  "get_top_movers",
  "Lấy danh sách top cổ phiếu tăng/giảm mạnh nhất phiên",
  {
    type: z.enum(["gainers", "losers"]).describe("Loại: gainers (tăng) hoặc losers (giảm)"),
    exchange: z.enum(["HOSE", "HNX", "UPCOM"]).default("HOSE")
      .describe("Sàn giao dịch"),
    limit: z.number().int().min(5).max(20).default(10)
      .describe("Số lượng kết quả")
  },
  async ({ type, exchange, limit }) => {
    try {
      const data = await fetchWithRetry(
        `https://api.example.com/market/top/${type}?exchange=${exchange}&limit=${limit}`
      );

      const label = type === "gainers" ? "🟢 TOP TĂNG" : "🔴 TOP GIẢM";
      const lines = data.stocks.map((s: any, i: number) =>
        `${i + 1}. ${s.symbol.padEnd(5)} ${s.price.toLocaleString("vi-VN").padStart(8)} (${s.changePct > 0 ? "+" : ""}${s.changePct.toFixed(2)}%) KL: ${(s.volume / 1e3).toFixed(0)}K`
      );

      return {
        content: [{
          type: "text",
          text: `${label} — Sàn ${exchange}\n${lines.join("\n")}`
        }]
      };
    } catch (error) {
      return {
        content: [{ type: "text", text: "Không thể lấy dữ liệu top cổ phiếu. Vui lòng thử lại." }],
        isError: true
      };
    }
  }
);

// Resource: Danh sách mã cổ phiếu theo sàn
server.resource(
  "stock-list",
  "stocks://list/{exchange}",
  { description: "Danh sách tất cả mã cổ phiếu trên một sàn giao dịch" },
  async (uri, { exchange }) => {
    const data = await fetchWithRetry(
      `https://api.example.com/stock/list?exchange=${exchange}`
    );
    return {
      contents: [{
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify(data.symbols, null, 2)
      }]
    };
  }
);

// Khởi động server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("vnstock-mcp server đang chạy");
}

main().catch(console.error);

Đăng ký vnstock-mcp với Claude Code

# Build
cd vnstock-mcp && npm run build

# Đăng ký
claude mcp add vnstock node /path/to/vnstock-mcp/dist/index.js

# Sử dụng trong Claude Code
# "VN-Index hôm nay thế nào?"
# "Cho tôi xem thông tin cổ phiếu FPT"
# "Top 10 cổ phiếu tăng mạnh nhất sàn HOSE hôm nay"

Mở rộng thêm

Bạn có thể bổ sung nhiều tính năng hữu ích khác cho server chứng khoán này:

  • Dữ liệu lịch sử: Tool lấy biểu đồ giá theo ngày/tuần/tháng, tính SMA, EMA, RSI
  • Phân tích tài chính: Resource trả về báo cáo tài chính (doanh thu, lợi nhuận, EPS) từ các quý gần nhất
  • Cảnh báo giá: Tool đăng ký thông báo khi cổ phiếu chạm ngưỡng giá nhất định
  • So sánh cổ phiếu: Tool so sánh 2-3 mã cổ phiếu theo nhiều chỉ tiêu cùng lúc
  • Tin tức thị trường: Resource tổng hợp tin tức mới nhất liên quan đến mã cổ phiếu

Tổng kết và bước tiếp theo

Bạn đã hoàn thành xây dựng MCP Server đầu tiên với TypeScript. Hãy tóm tắt những gì đã học:

  • Kiến trúc MCP: Giao thức client-server chuẩn hóa, giao tiếp qua stdio hoặc HTTP SSE
  • Tools vs Resources: Tools cho hành động (có side effect), resources cho đọc dữ liệu (read-only)
  • Validation: Sử dụng Zod cho type-safe input validation, kết hợp business logic validation
  • Error handling: Log vào stderr/file, trả về isError cho AI xử lý graceful
  • Triển khai: Docker cho production, npm publish cho distribution, .mcp.json cho team

MCP đang phát triển nhanh chóng với hệ sinh thái ngày càng phong phú. Ngoài tools và resources, bạn có thể khám phá thêm prompts (template tái sử dụng) và sampling (cho phép server yêu cầu AI xử lý trung gian). Xem thêm tài liệu chính thức tại modelcontextprotocol.io và danh sách MCP Server cộng đồng tại GitHub.

Hãy bắt đầu bằng một server đơn giản giải quyết vấn đề thực tế của bạn, sau đó mở rộng dần. Khám phá thêm các hướng dẫn phát triển với Claude tại Thư viện Ứng dụng Claude.

Tính năng liên quan:MCP ServerTypeScriptTool DefinitionTestingDeployment

Bai viet co huu ich khong?

Bản quyền thuộc về tác giả. Vui lòng dẫn nguồn khi chia sẻ.

Bình luận (0)
Ảnh đại diện
Đăng nhập để bình luận...
Đăng nhập để bình luận
  • Đang tải bình luận...

Đăng ký nhận bản tin

Nhận bài viết hay nhất về sản phẩm và vận hành, gửi thẳng vào hộp thư của bạn.

Bảo mật thông tin. Hủy đăng ký bất cứ lúc nào. Chính sách bảo mật.