0

MCP (Model Context Protocol)

Mở đầu

Nếu bạn đang làm việc với AI agent hoặc RAG (Retrieval-Augmented Generation), chắc hẳn bạn đã từng gặp bài toán: làm sao để model kết nối với dữ liệu và công cụ bên ngoài một cách gọn gàng, thay vì viết function calling thủ công cho từng tool riêng lẻ? Đó chính là vấn đề mà MCP (Model Context Protocol) ra đời để giải quyết.

Bài viết này mình tổng hợp lại những gì đã tìm hiểu qua tài liệu chính thức của MCP, kết hợp với kinh nghiệm thực tế khi xây dựng một hệ thống MCP server + RAG pipeline cho bài toán tra cứu tài liệu lưu trữ tiếng Việt.


1. MCP là gì và tại sao nó xuất hiện

Định nghĩa

MCP là một chuẩn mở (open-source standard) giúp kết nối các AI application (như Claude, ChatGPT) với hệ thống bên ngoài — bao gồm:

  • Data sources: file local, database
  • Tools: search engine, calculator, API nội bộ
  • Workflows: các prompt hoặc quy trình được định nghĩa sẵn

Ẩn dụ "USB-C cho AI"

MCP thường được ví như cổng USB-C: trước đây mỗi thiết bị cần một loại cáp riêng, giờ có một chuẩn chung cho tất cả. Tương tự, trước MCP, mỗi AI application muốn tích hợp với N nguồn dữ liệu thì cần viết N tích hợp riêng biệt (bài toán N×M). MCP chuẩn hóa lớp giao tiếp này, giúp "build once, integrate everywhere".

Lợi ích theo từng vai trò

Vai trò Lợi ích
Developer Giảm thời gian và độ phức tạp khi build/tích hợp AI application
AI application / agent Truy cập được cả một hệ sinh thái data, tool, app có sẵn
End-user AI assistant có khả năng cao hơn, thao tác được trên dữ liệu thật của mình

Hiện tại MCP được hỗ trợ rộng rãi bởi các client lớn như Claude, ChatGPT, VS Code, Cursor — điều này khiến việc build một MCP server trở nên đáng đầu tư vì khả năng tái sử dụng cao.


2. Kiến trúc cốt lõi (Architecture)

Ba thành phần chính

  • Host: ứng dụng AI mà người dùng tương tác trực tiếp (ví dụ Claude Desktop)
  • Client: thành phần bên trong host, giữ kết nối 1-1 với một server
  • Server: chương trình cung cấp context — tools, resources, prompts — cho client

Vòng đời kết nối

Một kết nối MCP đi qua các bước cơ bản: initialize → trao đổi capability (client và server thống nhất những gì hỗ trợ) → bước vào giai đoạn hoạt động (client gọi tool, đọc resource, v.v.)

Ba primitive chính mà server cung cấp

  • Tools: hàm mà model có thể gọi để thực hiện hành động (ví dụ: tìm kiếm tài liệu)
  • Resources: dữ liệu tĩnh hoặc bán tĩnh mà client có thể đọc
  • Prompts: template prompt được server định nghĩa sẵn, tái sử dụng được

Transport: local vs remote

Đây là điểm quan trọng khi triển khai thực tế:

  • stdio (local): server chạy như subprocess, giao tiếp qua standard input/output — phù hợp khi server và client chạy trên cùng máy
  • Streamable HTTP (remote): server chạy như một service độc lập, client gọi qua HTTP — phù hợp khi cần triển khai production, nhiều client cùng kết nối

Trong project MCP + RAG mình đang làm (dùng FastMCP với streamable-HTTP transport), việc chọn remote transport là bắt buộc vì hệ thống cần phục vụ nhiều agent/client đồng thời, chứ không chỉ chạy local như một CLI tool.


3. Build một MCP server — từ ví dụ đơn giản đến thực tế

Bước khởi đầu

Một MCP server tối thiểu chỉ cần định nghĩa một tool và chạy lên. Ví dụ minh họa với FastMCP:

from fastmcp import FastMCP

mcp = FastMCP("archive-search-server")

@mcp.tool()
async def search_archives(query: str, top_k: int = 5) -> list[dict]:
    """Tìm kiếm tài liệu lưu trữ theo từ khóa hoặc ngữ nghĩa."""
    # gọi Qdrant, xử lý kết quả...
    return results

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

Đây là bước "hello world", nhưng khi đưa vào production thì có vài bài học đáng chia sẻ.

a. Gộp nhiều tool nhỏ thành một tool tổng hợp

Ban đầu hệ thống của mình có 4 tool MCP riêng biệt (tìm theo từ khóa, tìm ngữ nghĩa, lọc theo loại tài liệu, v.v.). Vấn đề là model phải tự quyết định gọi tool nào, dễ gọi sai hoặc gọi tuần tự không cần thiết.

Giải pháp: gộp lại thành một tool duy nhất search_archives, bên trong dùng asyncio.gather để fan-out song song các biến thể từ khóa và fallback semantic search:

async def search_archives(query: str) -> list[dict]:
    keyword_variants = generate_keyword_variants(query)
    tasks = [search_keyword(kw) for kw in keyword_variants]
    tasks.append(search_semantic_fallback(query))
    results = await asyncio.gather(*tasks, return_exceptions=True)
    return merge_and_rerank(results)

Cách này vừa giảm số lần model phải "suy nghĩ chọn tool", vừa tăng tốc độ nhờ chạy song song.

b. Không block event loop

FastMCP chạy trên async event loop. Nếu bạn gọi một thư viện đồng bộ (ví dụ Qdrant client bản sync) trực tiếp, toàn bộ server sẽ bị block. Cách xử lý:

result = await asyncio.to_thread(qdrant_client.search, collection_name, query_vector)

c. Retry cho lỗi tạm thời

Với các lời gọi ra ngoài (API tài liệu, Qdrant), lỗi mạng tạm thời là chuyện thường gặp. Dùng tenacity để retry với exponential backoff giúp hệ thống ổn định hơn nhiều mà không cần code retry thủ công lặp lại ở nhiều nơi.

d. Cô lập lỗi khi ingest dữ liệu

Khi ingest hàng loạt tài liệu, một file lỗi không nên làm dừng cả pipeline. Bọc try/except ở cấp từng file, log lại và tiếp tục — nghe đơn giản nhưng rất dễ bị bỏ qua lúc mới build.


4. Kết nối client và vấn đề bảo mật/session

Local vs remote — khi nào dùng loại nào

  • Dùng local (stdio) khi server chỉ phục vụ một client duy nhất, chạy trên máy cá nhân, không cần quan tâm nhiều đến multi-user
  • Dùng remote (HTTP) khi cần phục vụ nhiều người dùng/agent, cần quản lý session, và có yêu cầu về access control

Vì sao bảo mật quan trọng hơn bạn nghĩ

Khi server có state và phục vụ nhiều người dùng, câu hỏi "request này là của ai" trở nên quan trọng. Trong quá trình làm semantic search fallback qua Qdrant, mình gặp một lỗ hổng access control: nếu không truyền identity người dùng một cách nhất quán, fallback search có thể trả về kết quả vượt quá quyền hạn của người gọi.

Có hai cách phổ biến để truyền identity:

  1. Qua tool parameter: model tự truyền user_id khi gọi tool — đơn giản nhưng dễ bị giả mạo nếu không kiểm soát chặt ở tầng client
  2. Qua HTTP session header: server tự lấy identity từ session đã xác thực — an toàn hơn vì không phụ thuộc vào việc model có "trung thực" truyền đúng tham số hay không

Bài học rút ra: không nên tin tưởng identity đến từ tham số do model sinh ra. Nếu có thể, ràng buộc identity ở tầng transport/session thay vì để model tự khai báo.

Vài nguyên tắc bảo mật ngắn gọn

  • Least privilege: server chỉ nên có quyền truy cập đúng phạm vi cần thiết
  • Validate input nghiêm ngặt, kể cả khi input đến từ model (model không phải là user đáng tin cậy tuyệt đối)
  • Không giả định client luôn gửi dữ liệu đúng định dạng — đã từng gặp lỗi serialize tham số keywords sai định dạng gây lỗi khó debug

5. Áp dụng MCP vào bài toán thực tế — RAG cho tài liệu lưu trữ

Vì sao chọn MCP thay vì gọi thẳng function

Hệ thống mình xây dựng phục vụ việc tra cứu tài liệu lưu trữ tiếng Việt, được nhiều agent/ứng dụng khác nhau sử dụng (không chỉ một chatbot cố định). MCP giúp:

  • Tách biệt logic tìm kiếm khỏi logic của từng agent — agent nào cũng gọi được cùng một search_archives tool
  • Chuẩn hóa interface, dễ mở rộng thêm client mới mà không phải sửa lại server

Kiến trúc rút gọn

Điểm đáng chú ý: kết quả tìm kiếm được overfetch nhiều hơn số lượng cần trả về, sau đó dùng cross-encoder để rerank rồi mới trim xuống top-k cuối cùng — giúp tăng độ chính xác so với chỉ dựa vào điểm số vector search thô.

Một lỗi đáng nhớ: point ID không ổn định

Ban đầu hệ thống dùng full URL (bao gồm cả host) làm point ID trong Qdrant. Khi đổi môi trường (host thay đổi), toàn bộ dữ liệu bị đánh index trùng lặp vì ID coi là khác nhau. Giải pháp là chuyển sang dùng path key độc lập với host làm ID — một bài học nhỏ nhưng tốn khá nhiều thời gian debug lúc phát hiện ra.


Kết luận

MCP không chỉ là một chuẩn giao tiếp kỹ thuật, mà còn thay đổi cách mình thiết kế hệ thống AI: tách bạch rõ ràng giữa "nơi cung cấp context" và "nơi ra quyết định". Với các bài toán RAG production như tra cứu tài liệu lưu trữ, việc đầu tư đúng vào tầng MCP server (async, retry, security) mang lại lợi ích rõ rệt về độ ổn định và khả năng mở rộng.

Tài liệu tham khảo: modelcontextprotocol.io/docs


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.