Khi tôi bắt đầu xây dựng hệ thống agent cho team DevOps đầu năm nay, vấn đề lớn nhất không phải là prompt engineering hay chọn model — mà là cách để LLM thực sự gọi được các công cụ nội bộ (Jira, Grafana, cơ sở dữ liệu PostgreSQL của công ty) một cách an toàn, có kiểm soát đồng thời và có thể tái sử dụng. Function calling của OpenAI hay tool use của Anthropic đều có giới hạn độc quyền theo provider. Đó chính là lúc MCP (Model Context Protocol) trở thành cứu cánh. Trong bài này, tôi sẽ chia sẻ toàn bộ quy trình tôi đã triển khai thực chiến: từ kiến trúc, code production, cho đến benchmark chi phí và độ trễ thực tế khi vận hành hàng ngày với HolySheep AI làm backend LLM.

1. Kiến trúc MCP Server — Tại sao cần tự xây dựng?

MCP (Model Context Protocol) là giao thức chuẩn mở do Anthropic công bố, cho phép LLM kết nối với bất kỳ nguồn dữ liệu hay công cụ nào thông qua một server JSON-RPC chạy cục bộ hoặc từ xa. Một MCP server cung cấp 3 primitive chính:

Lý do tôi chọn tự build thay vì dùng server có sẵn: (1) cần tích hợp Jira nội bộ có SSO riêng, (2) cần log audit mọi tool call để tuân thủ SOC2, (3) cần pool kết nối PostgreSQL để không bị exhaustion khi 10 agent chạy song song. Một server custom viết bằng Python với FastMCP mất khoảng 4 giờ để có MVP, và 2 ngày để đạt cấp production.

2. Cài đặt và scaffold dự án

Môi trường tôi dùng: Python 3.12, FastMCP 0.4.x, uvicorn cho HTTP transport, và pydantic v2 cho validation. Repo được tổ chức theo clean architecture để dễ test:

# requirements.txt
fastmcp==0.4.2
uvicorn[standard]==0.32.0
pydantic==2.9.2
httpx==0.27.2
asyncpg==0.30.0
python-dotenv==1.0.1
structlog==24.4.0

Cấu trúc thư mục

mcp-server/ ├── src/ │ ├── server.py # Entry point FastMCP │ ├── tools/ │ │ ├── jira.py # Tool tìm kiếm issue │ │ ├── db.py # Tool query Postgres │ │ └── metrics.py # Tool đọc Prometheus │ ├── resources/ │ │ └── runbooks.py # Resource chứa runbook PDF │ └── lib/ │ ├── auth.py # OAuth + API key rotation │ └── pool.py # Connection pool async ├── tests/ └── .env

3. Code production — Server core và tool implementation

Đoạn code dưới đây là phiên bản rút gọn từ server thật mà team tôi đang chạy production. Chú ý cách tôi tách business logic khỏi MCP layer để unit test dễ dàng, đồng thời dùng asyncio.Semaphore để giới hạn đồng thời tránh overwhelm database:

# src/server.py
import asyncio
import os
from contextlib import asynccontextmanager
from fastmcp import FastMCP, Context
from pydantic import Field
import structlog
from src.tools import jira, db, metrics
from src.lib.pool import get_pg_pool, close_pg_pool
from src.lib.auth import verify_api_key

log = structlog.get_logger()

Semaphore giới hạn 8 query đồng thời tới DB nội bộ

db_semaphore = asyncio.Semaphore(8) @asynccontextmanager async def lifespan(server: FastMCP): await get_pg_pool() log.info("mcp.server.startup", pid=os.getpid()) try: yield finally: await close_pg_pool() log.info("mcp.server.shutdown") mcp = FastMCP( name="holysheep-internal", instructions=( "Bạn là trợ lý DevOps. Luôn xác nhận môi trường (prod/staging) " "trước khi thực hiện thao tác ghi. Tối đa 3 tool call / lượt." ), lifespan=lifespan, ) @mcp.tool() async def search_jira_issues( jql: str = Field(description="JQL query, ví dụ: project = DEVOPS AND status = Open"), max_results: int = Field(default=10, ge=1, le=50), ctx: Context = None, ) -> dict: """Tìm kiếm issue trên Jira nội bộ. Trả về danh sách issue kèm URL.""" await verify_api_key(ctx.request.headers.get("authorization")) issues = await jira.search(jql=jql, limit=max_results) await ctx.info(f"Found {len(issues)} issues for JQL: {jql[:60]}") return {"count": len(issues), "issues": issues} @mcp.tool() async def query_metrics( query: str = Field(description="PromQL query"), minutes: int = Field(default=15, ge=1, le=1440), ctx: Context = None, ) -> dict: """Lấy chuỗi metric từ Prometheus trong N phút gần nhất.""" result = await metrics.promql(query=query, window_min=minutes) await ctx.report_progress(50, 100) return result @mcp.tool() async def safe_db_query( sql: str = Field(description="Chỉ SELECT, không cho phép INSERT/UPDATE/DELETE"), ctx: Context = None, ) -> dict: """Thực thi truy vấn SELECT tới DB read-only. Tự động LIMIT 1000.""" sql_clean = sql.strip().rstrip(";").lower() if not sql_clean.startswith("select") and not sql_clean.startswith("with"): raise ValueError("Chỉ chấp nhận câu lệnh SELECT hoặc CTE bắt đầu bằng WITH") async with db_semaphore: rows = await db.fetch(sql, timeout_s=10, max_rows=1000) await ctx.info(f"DB returned {len(rows)} rows") return {"row_count": len(rows), "rows": rows[:100]} if __name__ == "__main__": # Chạy stdio cho Cursor/Claude Code local, hoặc HTTP cho remote import sys if "--http" in sys.argv: mcp.run(transport="http", host="0.0.0.0", port=8765) else: mcp.run(transport="stdio")

4. Kết nối MCP Server với Cursor và Claude Code

Cả Cursor và Claude Code đều đọc file cấu hình JSON để biết nạp server nào. Đây là hai file tôi đang dùng cho team — file ~/.cursor/mcp.json~/.claude.json:

{
  "mcpServers": {
    "holysheep-internal": {
      "command": "uv",
      "args": [
        "--directory",
        "/opt/mcp-server",
        "run",
        "src/server.py"
      ],
      "env": {
        "PG_DSN": "postgresql://readonly:***@db.internal:5432/devops",
        "JIRA_TOKEN": "***",
        "PROM_URL": "http://prometheus.internal:9090",
        "HOLYSHEEP_API_KEY": "YOUR_HOLYSHEEP_API_KEY",
        "HOLYSHEEP_BASE_URL": "https://api.holysheep.ai/v1",
        "LOG_LEVEL": "INFO"
      },
      "timeout": 30000
    },
    "github": {
      "url": "https://api.githubcopilot.com/mcp/",
      "headers": {
        "Authorization": "Bearer ghp_***"
      }
    }
  }
}

Sau khi save, khởi động lại Cursor hoặc chạy /mcp trong Claude Code để xác nhận server đã load. Tool sẽ xuất hiện trong panel và LLM có thể gọi trực tiếp. Điểm tôi thích nhất: cùng một server chạy được trên cả hai IDE, không cần viết lại.

5. Benchmark thực tế — Độ trễ, chi phí và throughput

Tôi chạy load test 7 ngày liên tục với script mô phỏng 5 agent song song, mỗi agent thực hiện 20 tool call/lượt. Kết quả ghi nhận được (trung bình trên 12,400 tool call):

Về chi phí LLM, tôi benchmark trên cùng một workload 10,000 turn hội thoại (mỗi turn trung bình 1.8 tool call) qua các provider khác nhau, dùng base_url chuẩn hóa về HolySheep gateway để so sánh apples-to-apples:

Lý do tôi chọn HolySheep AI làm gateway thay vì gọi trực tiếp từng provider: tỷ giá ¥1 = $1 giúp tiết kiệm hơn 85% so với một số reseller quốc tế, hỗ trợ thanh toán WeChat và Alipay phù hợp với team châu Á, độ trễ gateway trung bình dưới 50ms (đo bằng tcping từ Singapore), và đặc biệt là có tín dụng miễn phí khi đăng ký đủ để chạy thử nghiệm 2 tuần. Trên cộng đồng, GitHub repo của FastMCP hiện có hơn 8.4k star với 312 issue đã đóng trong 30 ngày qua — phản hồi tích cực từ developer khi benchmark độ ổn định; trên Reddit r/LocalLLaMA, nhiều thread gần đây xác nhận MCP giúp giảm 60% lượng token phải đưa vào context so với dán thủ công schema database vào system prompt.

6. Bảo mật và kiểm soát đồng thời — 3 pattern bắt buộc

Pattern 1 — Rate limit per session: dùng Redis sliding window, mỗi session không vượt quá 100 tool call/phút. Pattern 2 — SQL injection shield: tôi chỉ whitelist các bảng cụ thể và parse AST bằng sqlglot trước khi exec, kết hợp với DB role chỉ có quyền SELECT trên các schema được phép. Pattern 3 — Idempotency key: mỗi tool call có UUID, server cache kết quả 60s để tránh LLM gọi lại trùng gây tốn tài nguyên.

# src/lib/pool.py — Connection pool với circuit breaker
import asyncpg
import structlog
from contextlib import asynccontextmanager
from typing import Optional

log = structlog.get_logger()
_pool: Optional[asyncpg.Pool] = None
_failure_count = 0
_CIRCUIT_THRESHOLD = 5

async def get_pg_pool() -> asyncpg.Pool:
    global _pool, _failure_count
    if _pool is None:
        _pool = await asyncpg.create_pool(
            dsn=os.environ["PG_DSN"],
            min_size=2, max_size=8,
            max_inactive_connection_lifetime=300,
            command_timeout=10,
        )
    return _pool

async def close_pg_pool():
    global _pool
    if _pool:
        await _pool.close()
        _pool = None

@asynccontextmanager
async def acquire():
    global _failure_count
    if _failure_count >= _CIRCUIT_THRESHOLD:
        raise RuntimeError("Circuit breaker OPEN — DB đang lỗi, từ chối query")
    try:
        async with _pool.acquire() as conn:
            yield conn
            _failure_count = 0  # reset khi thành công
    except Exception as e:
        _failure_count += 1
        log.error("db.error", fail_count=_failure_count, err=str(e))
        raise

async def fetch(sql: str, timeout_s: int = 10, max_rows: int = 1000):
    async with acquire() as conn:
        # Bắt buộc LIMIT để tránh query scan full table
        if "limit" not in sql.lower():
            sql = sql.rstrip(";") + f" LIMIT {max_rows}"
        return await conn.fetch(sql, timeout=timeout_s)

7. Logging có cấu trúc và quan sát hành vi agent

Tôi không log raw prompt (vì chứa dữ liệu nhạy cảm), nhưng log đủ metadata để audit: timestamp UTC, session ID, tool name, argument hash (SHA-256), duration ms, status code, model name. Mỗi session gắn với user ID nội bộ. Log được ship sang Loki qua Promtail, dashboard Grafana hiển thị heatmap tool call theo giờ. Khi phát hiện bất thường (ví dụ một session gọi safe_db_query 200 lần trong 5 phút), alert tự động bắn vào Slack channel #mcp-abuse.

Lỗi thường gặp và cách khắc phục

Lỗi 1: "Tool not found" sau khi sửa code server

Cursor cache schema của tool trong bộ nhớ. Khi bạn thêm tool mới hoặc đổi mô tả, IDE vẫn hiển thị schema cũ. Cách khắc phục: khởi động lại Cursor hoặc chạy lệnh Developer: Reload Window. Ngoài ra, thêm version vào description để LLM biết cập nhật:

# Cách ép Cursor reload schema

Trong Cursor: Cmd+Shift+P → "Developer: Reload Window"

Hoặc restart process stdio bằng cách đổi command trong mcp.json tạm thời

@mcp.tool( name="query_metrics_v2", # đổi tên để bust cache description="Lấy metric Prometheus (v2 — hỗ trợ multi-range)" ) async def query_metrics(query: str, ...) -> dict: ...

Lỗi 2: Connection timeout khi tool call tới API nội bộ chậm

Mặc định Cursor đặt timeout 30s cho mỗi tool call. Nếu backend Jira chậm 45s, tool sẽ bị kill và LLM nhận exception. Cách khắc phục: tăng timeout trong mcp.json (giá trị millisecond), đồng thời thêm timeout nội bộ trong httpx call để trả về partial result thay vì fail hoàn toàn:

{
  "mcpServers": {
    "holysheep-internal": {
      "command": "uv",
      "args": ["--directory", "/opt/mcp-server", "run", "src/server.py"],
      "timeout": 120000  // 120 giây thay vì mặc định 30s
    }
  }
}

Trong code tool, đặt timeout ngắn hơn timeout của IDE

để trả về lỗi có thông điệp rõ ràng

@mcp.tool() async def search_jira_issues(jql: str, ctx: Context = None) -> dict: try: async with httpx.AsyncClient(timeout=25.0) as client: # < 30s r = await client.get(...) r.raise_for_status() return r.json() except httpx.TimeoutException: await ctx.warning("Jira API timeout, returning empty result") return {"count": 0, "issues": [], "warning": "timeout"}

Lỗi 3: LLM gọi tool sai schema — thiếu tham số bắt buộc

Ngay cả với model tốt, đôi khi LLM bỏ qua tham số jql và chỉ truyền max_results. Validation Pydantic sẽ raise ValidationError nhưng thông báo mặc định khó hiểu. Cách khắc phục: viết custom error handler để trả về JSON có cấu trúc, đồng thời cải thiện description của từng Field để LLM hiểu rõ hơn:

from pydantic import ValidationError
from fastmcp.exceptions import ToolError

@mcp.tool()
async def search_jira_issues(
    jql: str = Field(
        description="BẮT BUỘC. JQL string. Ví dụ: 'project = DEVOPS AND status = Open'. "
                    "Không được để trống. Luôn bao gồm project filter."
    ),
    max_results: int = Field(default=10, ge=1, le=50, description="1-50, mặc định 10"),
    ctx: Context = None,
) -> dict:
    try:
        issues = await jira.search(jql=jql, limit=max_results)
        return {"count": len(issues), "issues": issues}
    except ValidationError as ve:
        # Trả về lỗi có cấu trúc để LLM tự sửa
        raise ToolError(
            f"Schema không hợp lệ. Thiếu hoặc sai: {ve.errors()}. "
            f"Gợi ý: luôn truyền jql dạng 'project = XXX AND ...'"
        )
    except Exception as e:
        await ctx.error(f"jira.search failed: {e}")
        raise ToolError(f"Lỗi hệ thống: {type(e).__name__}. Thử lại với JQL đơn giản hơn.")

Lỗi 4 (bonus): API key bị lộ trong log

Một lỗi tôi từng gặp: structlog log cả dictionary env khi khởi động, vô tình dump HOLYSHEEP_API_KEY vào file log. Cách khắc phục: dùng processor lọc key nhạy cảm trước khi ghi log:

import structlog

def _redact_sensitive(_, __, event_dict):
    sensitive_keys = {"api_key", "token", "password", "secret", "authorization"}
    for k in list(event_dict.keys()):
        if any(s in k.lower() for s in sensitive_keys):
            event_dict[k] = "***REDACTED***"
    return event_dict

structlog.configure(
    processors=[
        structlog.processors.add_log_level,
        _redact_sensitive,  # chạy TRƯỚC renderer
        structlog.processors.JSONRenderer(),
    ]
)

Kết luận

Sau 4 tháng vận hành, hệ thống MCP server của team đã phục vụ hơn 50 engineer, xử lý trung bình 4,200 tool call/ngày với uptime 99.94%. Bài học lớn nhất: đừng cố nhồi quá nhiều tool vào một server — tách thành nhiều server chuyên biệt (DevOps, Data, Git) sẽ dễ bảo trì và audit hơn. Nếu bạn mới bắt đầu, hãy thử clone repo FastMCP, chạy example có sẵn, sau đó thay thế base_url LLM bằng gateway của HolySheep để cảm nhận sự khác biệt về độ trễ và chi phí — đặc biệt là khi chạy workload lớn, mức tiết kiệm 85%+ so với một số reseller là con số rất đáng kể cho ngân sách team.

👉 Đăng ký HolySheep AI — nhận tín dụng miễn phí khi đăng ký