Xây dựng MCP Server tùy chỉnh với Python: Kết nối AI với hệ thống của bạn

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

Mấy tháng trước, đồng nghiệp mình than phiền rằng dù dùng Claude để hỗ trợ công việc, anh ấy vẫn phải mất cả buổi copy-paste log từ server vào chat, rồi lại copy kết quả phân tích ra để báo cáo. “Mình muốn Claude đọc thẳng từ server của mình luôn được không?” — câu hỏi đó khiến mình bắt đầu tìm hiểu về MCP.

Vấn đề này không phải chỉ anh ấy gặp. Phần lớn AI model đang chạy kiểu “hộp kín”: biết nhiều thứ từ training data, nhưng mù tịt với hệ thống của bạn. Database nội bộ, thư mục file server, API riêng của công ty — tất cả đều ngoài tầm với. Cần một cầu nối. MCP Server chính là thứ đó.

MCP là gì và tại sao cần nó?

MCP — viết tắt của Model Context Protocol — là giao thức mở do Anthropic phát triển, cho phép AI model kết nối với công cụ và nguồn dữ liệu bên ngoài theo cách chuẩn hóa.

Hình dung thế này: USB là chuẩn kết nối phần cứng — chuột, bàn phím, ổ cứng đều cắm qua một cổng duy nhất. MCP đóng vai trò tương tự cho AI. Bất kỳ client nào hỗ trợ MCP đều kết nối được với bất kỳ MCP server nào, không cần tích hợp riêng từng cặp.

Ba thành phần trong kiến trúc MCP:

  • MCP Host: Ứng dụng chạy AI — Claude Desktop, Cursor, hay app tự xây
  • MCP Client: Phần xử lý giao tiếp MCP bên trong Host
  • MCP Server: Server của bạn — expose tools, resources, prompts cho AI

Server có thể cung cấp ba loại thứ cho AI:

  • Tools: Hàm AI có thể gọi (đọc file, query database, gọi API…)
  • Resources: Dữ liệu AI đọc theo URI — tương tự như đọc một URL
  • Prompts: Template prompt tái sử dụng cho các tác vụ lặp lại

Xây dựng MCP Server đầu tiên bằng Python

Ta sẽ xây một “File Manager” — server cho phép AI liệt kê thư mục và đọc nội dung file. Đơn giản, nhưng đủ để thấy MCP hoạt động ra sao. Server này mình đang dùng hàng ngày để phân tích log: thay vì copy-paste hàng trăm dòng vào chat, Claude tự đọc thẳng từ /var/log/ và phân tích tại chỗ.

Bước 1: Cài đặt MCP Python SDK

Tạo môi trường ảo và cài thư viện:

python -m venv venv
source venv/bin/activate  # Linux/macOS
# venv\Scripts\activate   # Windows

pip install mcp

Bước 2: Tạo MCP Server cơ bản

Tạo file file_manager_server.py:

from mcp.server.fastmcp import FastMCP
import os
import json
import logging
from pathlib import Path
import datetime

# Cấu hình logging để theo dõi mọi tool call
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
log = logging.getLogger(__name__)

# Khởi tạo server với tên hiển thị
mcp = FastMCP("File Manager")

# Định nghĩa thư mục được phép truy cập (quan trọng cho bảo mật)
ALLOWED_DIR = Path("/home/user/documents").resolve()


def _is_safe_path(filepath: str) -> bool:
    """Kiểm tra đường dẫn có nằm trong thư mục cho phép không"""
    try:
        target = Path(filepath).resolve()
        return str(target).startswith(str(ALLOWED_DIR))
    except Exception:
        return False


@mcp.tool()
def list_files(directory: str) -> str:
    """Liệt kê file và thư mục trong đường dẫn chỉ định."""
    if not _is_safe_path(directory):
        return "Lỗi: Không có quyền truy cập thư mục này"

    log.info(f"list_files: {directory}")
    try:
        path = Path(directory)
        items = []
        for item in sorted(path.iterdir()):
            items.append({
                "name": item.name,
                "type": "directory" if item.is_dir() else "file",
                "size_kb": round(item.stat().st_size / 1024, 2) if item.is_file() else None
            })
        return json.dumps({"path": directory, "items": items, "count": len(items)}, ensure_ascii=False)
    except Exception as e:
        return f"Lỗi: {str(e)}"


@mcp.tool()
def read_file(filepath: str, max_lines: int = 100) -> str:
    """Đọc nội dung file text. max_lines mặc định 100 dòng để tránh tràn context."""
    if not _is_safe_path(filepath):
        return "Lỗi: Không có quyền truy cập file này"

    log.info(f"read_file: {filepath} (max_lines={max_lines})")
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            lines = f.readlines()

        total_lines = len(lines)
        content = ''.join(lines[:max_lines])

        if total_lines > max_lines:
            content += f"\n... (còn {total_lines - max_lines} dòng nữa, tăng max_lines nếu cần)"

        return content
    except UnicodeDecodeError:
        return "Lỗi: File không phải text hoặc encoding không hỗ trợ"
    except Exception as e:
        return f"Lỗi: {str(e)}"


@mcp.tool()
def get_file_info(filepath: str) -> str:
    """Lấy metadata của file: kích thước, thời gian sửa đổi cuối."""
    if not _is_safe_path(filepath):
        return "Lỗi: Không có quyền truy cập"

    try:
        stat = Path(filepath).stat()
        return json.dumps({
            "path": filepath,
            "size_bytes": stat.st_size,
            "size_kb": round(stat.st_size / 1024, 2),
            "modified": datetime.datetime.fromtimestamp(stat.st_mtime).isoformat(),
            "is_file": Path(filepath).is_file()
        }, ensure_ascii=False)
    except Exception as e:
        return f"Lỗi: {str(e)}"


if __name__ == "__main__":
    mcp.run()

Bước 3: Chạy thử server

Chạy trực tiếp để kiểm tra không có lỗi cú pháp:

python file_manager_server.py

Terminal im lặng hoàn toàn — đúng rồi đó, không phải lỗi. Giao tiếp MCP dùng stdin/stdout theo chuẩn JSON-RPC, server chờ kết nối chứ không in gì ra màn hình như web server thông thường.

Bước 4: Kết nối với Claude Desktop

Mở file cấu hình Claude Desktop. Trên macOS:

nano ~/Library/Application\ Support/Claude/claude_desktop_config.json

Trên Linux:

nano ~/.config/claude/claude_desktop_config.json

Thêm server của bạn vào:

{
  "mcpServers": {
    "file-manager": {
      "command": "python",
      "args": ["/đường/dẫn/đến/file_manager_server.py"]
    }
  }
}

Khởi động lại Claude Desktop. Icon MCP (hình búa) xuất hiện trong chat — từ lúc này, Claude gọi được tools của bạn trực tiếp trong hội thoại, không cần copy-paste gì thêm.

Mở rộng: Expose Resource cho dữ liệu tĩnh

Tools không phải thứ duy nhất bạn có thể expose. Resources cho phép AI đọc dữ liệu theo URI kiểu config://app-settings — hữu ích khi muốn AI luôn có context về hệ thống mà không cần hỏi mỗi lần. Ví dụ expose config file để Claude biết cấu hình app hiện tại:

@mcp.resource("config://app-settings")
def get_app_config() -> str:
    """Trả về cấu hình ứng dụng hiện tại"""
    config_path = ALLOWED_DIR / "config.json"
    if config_path.exists():
        return config_path.read_text(encoding='utf-8')
    return "{}"

AI đọc config://app-settings như đọc URL — sạch hơn nhiều so với nhớ đường dẫn file rồi truyền vào tool thủ công mỗi lần.

Những điểm cần lưu ý khi triển khai thực tế

Sau thời gian dùng MCP server tự xây, mình gặp một số vấn đề mà nếu biết sớm hơn đã tiết kiệm được kha khá thời gian debug:

  • Bắt buộc validate input: AI có thể truyền bất kỳ string nào vào tool — kể cả ../../etc/passwd. Hàm _is_safe_path() trong ví dụ ngăn AI ra ngoài thư mục cho phép. Thiếu cái này là lỗ hổng bảo mật nghiêm trọng, không phải cẩn thận thừa.
  • Giới hạn kích thước output: max_lines=100 quan trọng hơn bạn nghĩ. Đọc file log 100MB không giới hạn → context window tràn ngay, Claude trả lỗi, cả session đó coi như mất.
  • Trả về lỗi dạng string, không raise exception: AI đọc được message lỗi và thông báo lại cho người dùng. Raise exception thì Claude nhận thông báo chung chung, người dùng không biết chuyện gì xảy ra.
  • Log mọi tool call: Khi có vấn đề, bạn cần biết Claude gọi tool gì, tham số gì, lúc mấy giờ. Không có log, debug gần như mù.

Kết luận

Bản chất của MCP Server là một Python script expose các hàm thông thường theo chuẩn JSON-RPC — AI gọi chúng giống gọi API. Không có gì huyền bí. Biết viết Python function và xử lý được exception là đủ để xây MCP Server hoàn chỉnh.

Cái hay nhất của chuẩn MCP: viết server một lần, mọi client đều dùng được. Claude Desktop hôm nay, Cursor tuần sau, app nội bộ tháng tới — tất cả kết nối vào cùng một server, không cần viết lại integration.

Sau khi quen với file system, thử kết nối với SQLite hoặc PostgreSQL để Claude query database trực tiếp. Hoặc wrap API nội bộ của công ty thành MCP tools. Đồng nghiệp mình sau khi triển khai xong, giờ không còn phải copy-paste log nữa — Claude tự đọc, tự phân tích, anh ấy chỉ cần đọc kết quả.

Share: