Tôi có một agent đọc file, gọi GitHub API, tìm kiếm Slack. Ba tools, ba schema JSON, ba cách handle error khác nhau. Mỗi lần cần thêm tool mới, tôi phải viết lại schema từ đầu, wire vào agent loop, test riêng. Cộng thêm việc một đồng nghiệp muốn dùng cùng set tools đó trong agent khác của họ. Chúng tôi copy code.

Lần đó tôi mới thấy vấn đề: không có chuẩn chung cho “LLM gọi tool như thế nào”. Mỗi agent tự wire tool theo cách riêng. Tool viết cho agent A không chạy được trên agent B, dù cả hai dùng cùng model.

MCP (Model Context Protocol) giải quyết đúng chỗ đó.

MCP là gì

MCP là một open protocol do Anthropic publish năm 2024, định nghĩa chuẩn để LLM application (client) kết nối với external data source và tool (server). Mục tiêu: tách “tool logic” khỏi “agent logic” thành hai phần có thể phát triển độc lập.

Cách dễ nhớ nhất: MCP là USB-C cho AI tools. Trước USB-C, mỗi thiết bị có cổng riêng. Sau USB-C, bất kỳ thiết bị nào cắm vào cũng được. MCP làm điều tương tự: bất kỳ MCP client nào (Claude Desktop, Claude Code, custom agent) có thể kết nối với bất kỳ MCP server nào (filesystem, GitHub, Slack, database) mà không cần viết adapter riêng.

So sánh với cách cũ: function calling + JSON schema.

# Cách cũ: wire tool trực tiếp vào agent
tools = [
    {
        "name": "read_file",
        "description": "Read a file",
        "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}}
    }
]
# Tool schema nằm trong code agent. Agent A và agent B phải duplicate.

Với MCP: tool schema sống trong MCP server. Agent chỉ connect đến server, tự discovery ra tool nào có sẵn. Agent A và agent B đều connect cùng một filesystem server, không ai phải duplicate gì.

Architecture: client, server, transport

Ba thành phần trong một MCP setup:

MCP Host: application tích hợp LLM, chứa một hoặc nhiều MCP client. Ví dụ: Claude Desktop, Claude Code, một custom Python agent.

MCP Client: component bên trong host, maintain kết nối 1-1 với một MCP server. Host có thể có nhiều client, mỗi client nói chuyện với một server khác nhau.

MCP Server: process cung cấp tools, resources, và prompts. Có thể là local process (filesystem server chạy trên máy) hoặc remote service (GitHub server chạy trên cloud).

Transport: kênh giao tiếp giữa client và server. Hai loại:

  • stdio: client spawn server như subprocess, giao tiếp qua stdin/stdout. Dùng cho local server.
  • SSE (Server-Sent Events): giao tiếp qua HTTP. Dùng cho remote server.
[Claude Desktop]
    |
    |-- MCP Client A --> stdio --> [filesystem server (local)]
    |
    |-- MCP Client B --> SSE  --> [github server (remote)]
    |
    |-- MCP Client C --> stdio --> [slack server (local)]

Khi user chat với Claude Desktop, Claude biết nó có ba server kết nối. Khi Claude muốn đọc file, nó gọi tool read_file qua MCP Client A. Response đi ngược lại theo cùng transport.

Ba entity trong MCP: Tools, Resources, Prompts

MCP không chỉ chuẩn hoá tool calling. Nó định nghĩa ba loại entity mà server có thể cung cấp:

EntityĐịnh nghĩaVí dụLLM trigger
ToolsFunction LLM gọi để thực hiện actionread_file, create_issue, send_messageLLM chủ động chọn gọi
ResourcesData source LLM đọc như contextFile content, DB row, API responseUser hoặc app expose
PromptsTemplate prompt có sẵn trong serversummarize_pr, explain_errorUser chọn từ UI

Tool là phần dễ liên hệ nhất với function calling cũ. Resources thay cho cơ chế “nhồi data vào context trước khi gọi LLM”. Prompts là workflow templates được đặt ở server, không phải hardcode trong application.

Trong thực tế, phần lớn MCP server public chủ yếu expose Tools. Resources và Prompts phổ biến hơn ở enterprise use case (nơi cần chuẩn hoá cả context và workflow).

Ecosystem: Anthropic-maintained và community

Anthropic duy trì một số MCP server chính thức:

  • filesystem: đọc/ghi file local, với whitelist thư mục
  • github: repo, issues, pull requests, file content
  • slack: gửi message, đọc channel, search
  • postgres: query database
  • puppeteer: browser automation
  • brave-search: web search

Cộng đồng đã build thêm hàng trăm server: Linear, Jira, Notion, AWS, Kubernetes, Redis, và nhiều loại khác. Một bộ sưu tập server cộng đồng có thể tìm ở github.com/modelcontextprotocol/servers.

Điều quan trọng: bạn có thể build MCP server của riêng mình. Nếu internal API của công ty chưa có server, bạn tự build, connect Claude Desktop hoặc Claude Code vào, và team dùng ngay mà không cần cài thêm gì trong agent code.

Claude Code sử dụng MCP native. Tất cả tool trong Claude Code (Bash, Read, Edit, Write, Grep) thực ra đều là MCP tools phía dưới. Bài Anthropic SDK agents và Claude Code agents sẽ đi sâu vào phần này.

Build MCP server đơn giản

MCP Python SDK (pip install mcp) cung cấp decorator để build server với rất ít code.

Ví dụ: một server expose hai tools, đọc file và list directory:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("local-fs")

@mcp.tool()
def read_file(path: str) -> str:
    """Read content of a file at the given path."""
    with open(path, "r") as f:
        return f.read()

@mcp.tool()
def list_dir(path: str) -> list[str]:
    """List entries in a directory."""
    import os
    return os.listdir(path)

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

Chạy server: python server.py. Server lắng nghe trên stdio, sẵn sàng nhận connection từ MCP client.

Để connect Claude Desktop đến server này, thêm vào ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "local-fs": {
      "command": "python",
      "args": ["/path/to/server.py"]
    }
  }
}

Sau khi restart Claude Desktop, tools read_filelist_dir xuất hiện trong Claude như native tools. Không cần thay đổi gì trong cách prompt Claude.

TypeScript SDK tương tự:

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: "local-fs", version: "1.0.0" });

server.tool(
  "read_file",
  { path: z.string() },
  async ({ path }) => {
    const fs = await import("fs/promises");
    const content = await fs.readFile(path, "utf-8");
    return { content: [{ type: "text", text: content }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

So sánh MCP với function calling cổ điển

DimensionFunction calling cổ điểnMCP
Tool schema nằm ở đâuTrong code agentTrong MCP server, agent tự discovery
Reuse giữa agentsCopy codeConnect cùng một server
TransportHTTP request tới Anthropic APIstdio hoặc SSE tới server riêng
StateStateless per callStateless mặc định (xem pitfall)
EcosystemMỗi team tự buildCó sẵn 200+ server công khai
HostingAgent host toolsServer host tools, agent chỉ connect
DebugLog trong agentLog trong server process riêng

Function calling không biến mất. MCP xây trên top của function calling. Bên trong, khi LLM gọi MCP tool, vẫn là một function call JSON dưới hood. MCP thêm layer discovery (client hỏi server “mày có tool gì”) và chuẩn hoá transport.

Để thiết kế tool schema tốt trước khi wire vào MCP, đọc bài 11: Tool design.

Pitfall: MCP server stateless mặc định

Đây là điều tôi va vào khi build một MCP server đầu tiên: MCP server stateless theo mặc định.

Tôi muốn server theo dõi “đã đọc file nào rồi” để tránh đọc lại. Code đơn giản:

# SẼ GÂY BUG
visited = set()  # State trong server

@mcp.tool()
def read_file(path: str) -> str:
    if path in visited:
        return "already read"
    visited.add(path)
    with open(path, "r") as f:
        return f.read()

Nhìn qua có vẻ đúng. Thực ra sai theo hai hướng.

Hướng 1: multiple clients. Nếu hai Claude Desktop sessions kết nối cùng server, visited được share. Session A đọc file X, session B hỏi file X, bị trả về “already read” dù session B chưa đọc lần nào.

Hướng 2: server restart. Nếu server crash và restart (Claude Desktop tự spawn lại process), visited bị reset, mất hết state. Agent bên ngoài không biết server đã restart, behavior thay đổi không dự đoán được.

Rule đúng: state thuộc về client, không phải server.

# ĐÚNG: server không giữ state
@mcp.tool()
def read_file(path: str) -> str:
    """Read file. Client phải tự track đã đọc file nào."""
    with open(path, "r") as f:
        return f.read()

Client (tức là agent code hoặc Claude Desktop) chịu trách nhiệm nhớ “đã gọi tool nào với input nào”. Server chỉ execute và trả kết quả. Cách này an toàn khi có multiple clients và khi server restart.

Nếu thực sự cần server-side state (ví dụ: session context cho database connection), dùng session ID do client truyền xuống, không dùng global variable trong server.

MCP trong Claude Code

Claude Code là một MCP client đầy đủ. Khi bạn configure MCP server trong ~/.claude/settings.json, Claude Code connect đến server đó và expose tools cho tất cả sessions.

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]
    }
  }
}

Sau khi thêm config này, trong Claude Code session bạn có thể nói “query bảng users” mà không cần giải thích connection string hay schema. Claude Code tự discovery tool query từ postgres server, gọi với đúng SQL.

Đây là lý do Claude Code có thể làm việc với nhiều loại environment khác nhau mà không cần cài extension hay plugin: chỉ cần một MCP server là đủ.

Cheatsheet MCP entities

EntityMethod discoveryReturn typeUse when
Tooltools/listCallToolResultLLM cần thực hiện action
Resourceresources/listReadResourceResultCần inject data vào context
Promptprompts/listGetPromptResultTemplate workflow chuẩn hoá
ResourceTemplateresources/templates/listURI templateResource với dynamic URI
TransportKhi nào dùngProsCons
stdioLocal serverĐơn giản, không cần portKhông remote
SSERemote serverCross-machine, multi-clientCần HTTP infra
PatternMô tả
Stateless serverServer không giữ state, client track history
Tool namespacingPrefix tool name: fs_read, gh_create_issue
Error propagationServer throw exception, client nhận isError: true
Capability negotiationClient và server negotiate feature khi connect

Lời kết

MCP giải quyết vấn đề mà function calling để lộ khi scale: tool logic bị gắn chặt vào agent code, khó share, khó test riêng, khó reuse. Protocol không thần kỳ gì. Nó chỉ đặt một layer abstraction đúng chỗ: tách tool hosting khỏi agent logic, define chuẩn giao tiếp giữa hai bên.

Khi build agent nghiêm túc, việc tách tool ra thành MCP server riêng thay vì hardcode trong agent loop sẽ giúp bạn test tool độc lập, share với nhiều agent, và update tool mà không deploy lại agent.

Bài 15 kết thúc Part 3 của series. Từ bài 11 đến 15, chúng ta đã cover tool design, code execution, browser automation, RAG, và giờ là chuẩn hoá tool layer với MCP.

Bài 16, Multi-agent patterns: supervisor, handoff, debate, mở đầu Part 4. Khi một agent không đủ, bạn cần nhiều agent phối hợp. Câu hỏi thực sự là: phối hợp như thế nào mà không tạo ra một mớ chaos phức tạp hơn vấn đề ban đầu.