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ĩa | Ví dụ | LLM trigger |
|---|---|---|---|
| Tools | Function LLM gọi để thực hiện action | read_file, create_issue, send_message | LLM chủ động chọn gọi |
| Resources | Data source LLM đọc như context | File content, DB row, API response | User hoặc app expose |
| Prompts | Template prompt có sẵn trong server | summarize_pr, explain_error | User 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_file và list_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
| Dimension | Function calling cổ điển | MCP |
|---|---|---|
| Tool schema nằm ở đâu | Trong code agent | Trong MCP server, agent tự discovery |
| Reuse giữa agents | Copy code | Connect cùng một server |
| Transport | HTTP request tới Anthropic API | stdio hoặc SSE tới server riêng |
| State | Stateless per call | Stateless mặc định (xem pitfall) |
| Ecosystem | Mỗi team tự build | Có sẵn 200+ server công khai |
| Hosting | Agent host tools | Server host tools, agent chỉ connect |
| Debug | Log trong agent | Log 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
| Entity | Method discovery | Return type | Use when |
|---|---|---|---|
Tool | tools/list | CallToolResult | LLM cần thực hiện action |
Resource | resources/list | ReadResourceResult | Cần inject data vào context |
Prompt | prompts/list | GetPromptResult | Template workflow chuẩn hoá |
ResourceTemplate | resources/templates/list | URI template | Resource với dynamic URI |
| Transport | Khi nào dùng | Pros | Cons |
|---|---|---|---|
| stdio | Local server | Đơn giản, không cần port | Không remote |
| SSE | Remote server | Cross-machine, multi-client | Cần HTTP infra |
| Pattern | Mô tả |
|---|---|
| Stateless server | Server không giữ state, client track history |
| Tool namespacing | Prefix tool name: fs_read, gh_create_issue |
| Error propagation | Server throw exception, client nhận isError: true |
| Capability negotiation | Client 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.