MCP Client — Cầu nối giao tiếp

Nền tảngCơ bản30 phút

Hãy tưởng tượng bạn đang đứng ở sân bay Hà Nội và muốn đến Tokyo. Bạn không cần hiểu:

Bạn sẽ học được
  • Giải thích vai trò MCP Client như cầu nối protocol giữa ứng dụng và MCP server
  • Hiểu "transport agnostic" nghĩa là gì và liệt kê 3 transport phổ biến (stdio, HTTP, WebSocket)
  • Nhận diện 4 message type quan trọng nhất trong MCP (ListTools, CallTool, ListResources, ReadResource)
  • Vẽ lại flow end-to-end từ user query → Claude → MCP → external API → trả lời
  • Phân biệt "MCP Client" (protocol abstraction) vs "your custom client class" (SDK wrapper)

MCP Client là gì — Định nghĩa chính xác

MCP Client là thành phần trong ứng dụng của bạn chịu trách nhiệm giao tiếp theo chuẩn MCP protocol với một MCP Server.

Về mặt kỹ thuật, MCP Client là:

Hai thuật ngữ cần phân biệt

Trong codebase thực tế, bạn sẽ gặp hai khái niệm "client" dễ nhầm:

Tại sao cần wrapper? Vì ClientSession cần careful resource management — phải properly cleanup khi kết thúc. Wrapping nó trong class của bạn giúp dùng async with để SDK tự handle cleanup, giảm bug leak connection.

  • Một session object giữ kết nối bền vững (stateful) với server
  • Một protocol translator dịch giữa API Python (ví dụ list_tools()) và các JSON-RPC message chuẩn
  • Một transport handler che giấu chuyện đang chạy qua stdio, HTTP, WebSocket, hay gì khác
  • ClientSession (từ MCP SDK) — Lớp protocol thật sự. Gửi/nhận JSON-RPC message, quản lý request ID, timeout.
  • Your custom MCPClient class — Wrapper do bạn viết thêm để dễ dùng cho use case cụ thể: quản lý cleanup, thêm retry, đóng gói resource management. Bạn sẽ build cái này trong Bài 7.6.
┌──────────────────────────────────────────────────────┐
│                                                      │
│           YOUR APPLICATION (Python code)             │
│                                                      │
│   ┌────────────────────────────────────────────┐     │
│   │                                            │     │
│   │    your MCPClient class (custom wrapper)   │     │
│   │    ├── Convenience methods cho app của bạn │     │
│   │    ├── Xử lý lifecycle / cleanup           │     │
│   │    └── Wrap ClientSession bên dưới         │     │
│   │                                            │     │
│   │    ┌──────────────────────────────────┐    │     │
│   │    │                                  │    │     │
│   │    │  ClientSession (from MCP SDK)    │    │     │
│   │    │  ├── Protocol layer              │    │     │
│   │    │  ├── Transport handler           │    │     │
│   │    │  └── Message serialization       │    │     │
│   │    │                                  │    │     │
│   │    └──────────────────────────────────┘    │     │
│   │                                            │     │
│   └────────────────────────────────────────────┘     │
│                       ▲                              │
│                       │ stdio / HTTP / WebSocket     │
└───────────────────────┼──────────────────────────────┘
                        │
                        ▼
                   MCP Server (process riêng)

Transport Agnostic — Sức mạnh ẩn giấu

Một trong những điểm mạnh lớn nhất của MCP là transport agnostic — một thuật ngữ nghe fancy nhưng rất thực dụng: client và server có thể giao tiếp qua nhiều giao thức khác nhau, và code của bạn không cần quan tâm.

Transport 1: stdio (Standard Input/Output)

Setup phổ biến nhất tính tới 2025 — chiếm đa số trong 10,000+ MCP server. Cả client và server chạy trên cùng 1 máy:

Khi nào dùng:

Ưu: Cực đơn giản, zero network config, bảo mật tự nhiên (không expose port). Nhược: Chỉ 1 user, không share được qua mạng.

Transport 2: Streamable HTTP

Spec 2025 bổ sung cho remote deployment. Server là một HTTP endpoint public:

Khi nào dùng:

Ưu: Scale như REST API thường (Lambda OK cho simple case), user không cần cài gì local. Nhược: Cần setup HTTPS, auth, rate limiting.

Transport 3: WebSocket / SSE

Cho stateful workloads real-time — streaming tokens, progress events, long-running tasks:

Khi nào dùng:

Ưu: Real-time. Low latency. Nhược: Phức tạp hơn HTTP, cần connection management.

Tại sao điều này quan trọng?

Code application của bạn viết một lần hoạt động trên mọi transport. Ví dụ:

Đây là lý do bạn thấy một MCP server chạy được ở cả Claude Desktop, Cursor, Zed, VS Code, và custom app — mỗi app là một client khác, nhưng protocol giống nhau.

  • Dev tool chạy local (filesystem access, git MCP, SQLite MCP)
  • Personal hardware integration (synth, Raspberry Pi)
  • Prototype nhanh, test
  • Vendor official server (GitHub, Canva, Stripe, Zapier)
  • Multi-user shared server
  • Enterprise internal server sau SSO
  • Server cần push event (notification, progress update, cancellation)
  • Long-running task (deep research, agent-to-agent orchestration)
  • Tương tác bidirectional liên tục
┌─────────────────────┐          ┌─────────────────────┐
│   MCP Client        │          │   MCP Server        │
│   (in your app)     │ ──────►  │   (mcp.github.com)  │
│                     │   HTTP   │                     │
│                     │ ◄──────  │                     │
└─────────────────────┘          └─────────────────────┘
     App ở máy bạn              Deployed ở cloud
# Local stdio
client = MCPClient("stdio://./my_server.py")

# Remote HTTP
client = MCPClient("https://mcp.github.com")

# Same code sau đó
tools = await client.list_tools()  # identical

MCP Message Types — Vocabulary cốt lõi

Khi client và server nói chuyện, họ trao đổi message types chuẩn hóa trong MCP spec. Với khóa học intro này, bạn cần thuộc 8 message quan trọng nhất:

Tool Messages — Ưu tiên cao nhất

ListToolsRequest / ListToolsResult

Client hỏi: "Server ơi, mày có tool gì?" Server trả về: danh sách tool với name, description, input schema.

Sau khi có list, client truyền vào Anthropic API để Claude biết tool nào có sẵn.

CallToolRequest / CallToolResult

Khi Claude quyết định cần gọi tool, client gửi request thực thi:

┌─────────────────────────────────────────────────────────┐
│                                                         │
│  TOOL MESSAGES (xử lý trong Bài 7.4, 7.7)               │
│  ┌─────────────────────────────────────────────┐        │
│  │  ListToolsRequest   ──►   ListToolsResult   │        │
│  │  CallToolRequest    ──►   CallToolResult    │        │
│  └─────────────────────────────────────────────┘        │
│                                                         │
│  RESOURCE MESSAGES (xử lý trong Bài 7.7, 7.9)           │
│  ┌─────────────────────────────────────────────┐        │
│  │  ListResourcesRequest  ──► ListResourcesResult       │
│  │  ReadResourceRequest   ──► ReadResourceResult │      │
│  └─────────────────────────────────────────────┘        │
│                                                         │
│  PROMPT MESSAGES (xử lý trong Bài 7.9, 7.11)           │
│  ┌─────────────────────────────────────────────┐        │
│  │  ListPromptsRequest  ──►   ListPromptsResult│        │
│  │  GetPromptRequest    ──►   GetPromptResult  │        │
│  └─────────────────────────────────────────────┘        │
│                                                         │
└─────────────────────────────────────────────────────────┘
Client ─► { "method": "tools/list" }
Server ◄─ {
  "tools": [
    { "name": "read_doc_contents",
      "description": "Read the contents of a document...",
      "inputSchema": { "type": "object", "properties": {...} }
    },
    ...
  ]
}

Tool Messages — Ưu tiên cao nhất

Server chạy hàm tương ứng, trả kết quả về. Client đưa kết quả lại cho Claude.

Client ─► { "method": "tools/call",
             "params": {
               "name": "read_doc_contents",
               "arguments": { "doc_id": "deposition.md" } }}
Server ◄─ { "content": [
              { "type": "text",
                "text": "This deposition covers..." }],
           "isError": false }

Flow end-to-end — Xem mọi thứ hoạt động cùng nhau

Đây là diagram quan trọng nhất bài này. Bạn sẽ thấy lại nó trong Bài 7.6 khi viết code thực tế. Hãy đọc kỹ.

Giả sử user hỏi: "What repositories do I have?"

11 bước trong 1 câu hỏi

Phân rã rõ ràng để bạn ghi nhớ:

Có vẻ nhiều bước, nhưng mỗi thành phần có một trách nhiệm rõ ràng. MCP client trừu tượng hóa độ phức tạp của giao tiếp server, để bạn tập trung vào application logic.

  • User query — User submit câu hỏi tới app của bạn
  • Tool discovery — App biết phải gửi danh sách tools cho Claude cùng query
  • List Tools Exchange — App gọi client.list_tools()
  • MCP Communication — Client gửi ListToolsRequest qua transport, nhận ListToolsResult
  • Claude Request — App POST query + tools schema lên Anthropic API
  • Tool Use Decision — Claude quyết định gọi get_repos() với args cụ thể
  • Tool Execution Request — App gọi client.call_tool("get_repos", args)
  • External API Call — MCP client gửi CallToolRequest → MCP server → GitHub API
  • Results Flow Back — GitHub response → MCP server wrap thành CallToolResult → back to client
  • Tool Result to Claude — App POST tool result về Anthropic API
  • Final Response — Claude formulate câu trả lời từ data → về user
 USER                   YOUR SERVER        MCP CLIENT         MCP SERVER       GITHUB API
  │                          │                  │                  │                │
  │ "What repos do I have?"  │                  │                  │                │
  ├─────────────────────────►│                  │                  │                │
  │                          │                  │                  │                │
  │                          │ list_tools()     │                  │                │
  │                          ├─────────────────►│                  │                │
  │                          │                  │ ListToolsRequest │                │
  │                          │                  ├─────────────────►│                │
  │                          │                  │ ListToolsResult  │                │
  │                          │                  │◄─────────────────┤                │
  │                          │ [tools list]     │                  │                │
  │                          │◄─────────────────┤                  │                │
  │                          │                  │                  │                │
  │                          │ POST /messages   │                  │                │
  │                          │   query + tools  │                  │                │
  │                          ├──► Claude API    │                  │                │
  │                          │                  │                  │                │
  │                          │ tool_use:        │                  │                │
  │                          │   get_repos()    │                  │                │
  │                          │◄── Claude API    │                  │                │
  │                          │                  │                  │                │
  │                          │ call_tool(...)   │                  │                │
  │                          ├─────────────────►│                  │                │
  │                          │                  │ CallToolRequest  │                │
  │                          │                  ├─────────────────►│                │
  │                          │                  │                  │ GET /repos     │
  │                          │                  │                  ├───────────────►│
  │                          │                  │                  │ [repo data]    │
  │                          │                  │                  │◄───────────────┤
  │                          │                  │ CallToolResult   │                │
  │                          │                  │◄─────────────────┤                │
  │                          │ [result]         │                  │                │
  │                          │◄─────────────────┤                  │                │
  │                          │                  │                  │                │
  │                          │ POST /messages   │                  │                │
  │                          │   + tool result  │                  │                │
  │                          ├──► Claude API    │                  │                │
  │                          │                  │                  │                │
  │                          │ "You have 42 repos│                  │                │
  │                          │   including..."   │                  │                │
  │                          │◄── Claude API    │                  │                │
  │                          │                  │                  │                │
  │  "You have 42 repos..."  │                  │                  │                │
  │◄─────────────────────────┤                  │                  │                │
  │                          │                  │                  │                │

Bảng so sánh: MCP Client vs các abstraction khác

MCP Client không thay thế các abstraction kia — nó là lớp chuyên biệt cho use case AI cần tool/context từ external source.

Tiêu chíMCP ClientHTTP Client (requests)gRPC StubWebhook Handler
Kiểu kết nốiStateful sessionStateless request/responseStateful streamInbound event
ProtocolJSON-RPC 2.0HTTP/JSONProtobufHTTP POST
Transportstdio/HTTP/WSHTTP(S)HTTP/2HTTP(S)
Discoverylist_tools, list_resourcesDocs/OpenAPI.proto fileConfig doc
AuthOAuth 2.1 (remote), none (local)Nhiều kiểuTLS mTLSSecret/signature
Tối ưu choModel ↔ tool/contextApp ↔ serverMicroserviceServer ↔ server
Ai dùng?AI agents, chat clientsMọi ngườiBackend teamsWebhooks integration

Ví dụ thực chiến: Debug một MCP client bị treo

Tình huống

Bạn chạy app của mình với local MCP server cho filesystem. App treo khi gọi list_tools(). Terminal không lỗi, chỉ ngồi chờ.

Bước 1: Check transport

Dùng mcp dev để verify server start được:

Nếu bước này fail, vấn đề nằm ở server code, không phải client.

Bước 2: Check client connection

Thêm log ngay khi client init:

uv run mcp dev mcp_server.py
# Phải in ra "Inspector running at http://127.0.0.1:6274"

Bước 2: Check client connection

Nếu "Connected!" không in ra → transport sai. Check path file server.

Bước 3: Check message flow

Dùng MCP Inspector (Bài 7.5) để xem message trao đổi. Open browser tới http://127.0.0.1:6274, click tab Tools, xem request/response.

Bước 4: Check timeout

Một số server async init chậm. Increase timeout:

import logging
logging.basicConfig(level=logging.DEBUG)

async with MCPClient("stdio://./mcp_server.py") as client:
    print("Connected!")  # Should print before list_tools
    tools = await client.list_tools()

Bước 4: Check timeout

Kết quả

90% MCP debug đi qua 4 bước này. Hiểu flow là 80% đường tới fix bug.

async with MCPClient(
    config,
    initialization_timeout=30.0  # default 5s
) as client:
    ...

Ví dụ theo ngành — Ai dùng MCP Client?

🛠️ Developer Tools — Cursor

Cursor IDE bundle sẵn MCP client. User config MCP server qua JSON, Cursor tự handle lifecycle. Khi dev chat với Claude trong Cursor, query đi qua MCP client → tool/resource → trả về. Dev không thấy client.

Đặc điểm: Mọi tool dev thêm (GitHub, Linear, local DB) đều qua cùng 1 client layer.

💬 Chat Apps — Claude Desktop

Claude Desktop là MCP client lớn nhất về số user. Khi bạn add MCP server qua Settings, Claude Desktop spawn subprocess, thiết lập stdio transport, và expose tool/resource trong UI.

Đặc điểm: End-user không thấy protocol. Chỉ thấy icon tool + slash command. Client giấu hết.

📋 Task Managers — Asana

Asana có MCP server. Michael Cohen (Anthropic) viết custom app với MCP client để kéo task từ Asana + Slack + codebase. Tất cả đi qua 1 wrapper client class.

Đặc điểm: Multi-server scenario. Một custom client quản lý nhiều MCP sessions đồng thời.

🎨 Design — Canva

Canva MCP client chạy trên server của Canva phía backend. End-user chat qua Claude.ai, request đi qua Canva's custom MCP client → Canva MCP server → design engine.

Đặc điểm: B2B2C pattern. User không biết MCP tồn tại. Vendor hide protocol hoàn toàn.

🏢 Enterprise — Internal SSO

Công ty triển khai MCP internal với Entra ID / Okta. Mỗi developer install app nội bộ có MCP client. Token SSO từ morning login flow vào mọi tool call.

Đặc điểm: Client carry auth context. Security nằm ở token, không ở code tool.

Anti-patterns — Những sai lầm cần tránh

❌ Không cleanup session

Sai lầm:

Tại sao là sai: Server process bị orphan. Lâu dần zombie processes. Memory leak.

Cách đúng: Dùng async with:

client = MCPClient("stdio://./server.py")
await client.initialize()
tools = await client.list_tools()
# Không bao giờ close

❌ Không cleanup session

❌ Mở session mới cho mỗi request

Sai lầm: Gọi list_tools() trước mỗi user query → spawn subprocess mới mỗi lần.

Tại sao là sai: Startup time 100-500ms mỗi lần. User feels lag. Overhead cao.

Cách đúng: Giữ session alive xuyên suốt app lifecycle. Reuse client cho mọi request.

❌ Ignore error từ CallToolResult

Sai lầm:

async with MCPClient("stdio://./server.py") as client:
    tools = await client.list_tools()
    # Auto cleanup khi ra khỏi block

❌ Ignore error từ CallToolResult

Tại sao là sai: CallToolResult có field isError. Nếu tool raise exception server-side, isError=True và content chứa error message. Bỏ qua → app nhận error text tưởng là data thật.

Cách đúng:

result = await client.call_tool("read_doc", {...})
return result.content[0].text  # Crash nếu server lỗi

Anti-patterns — Những sai lầm cần tránh (tiếp)

❌ Cứng hóa transport

Sai lầm:

result = await client.call_tool("read_doc", {...})
if result.isError:
    logger.error(f"Tool error: {result.content}")
    return None
return result.content[0].text

❌ Cứng hóa transport

Tại sao là sai: Khi cần migrate sang remote HTTP, phải refactor toàn bộ. Khó test với mock transport.

Cách đúng: Parameterize transport qua config:

# Chỉ hard-code stdio
client = StdioClient("./server.py")

Anti-patterns — Những sai lầm cần tránh (tiếp)

❌ Confuse Session với Client class

Sai lầm: Expose ClientSession trực tiếp trong app logic. Rải session.call_tool(...) khắp nơi.

Tại sao là sai: Khi SDK update, bạn phải sửa mọi call site. Không có nơi để thêm retry, logging, metrics chung.

Cách đúng: Wrap trong custom class (như Bài 7.6 sẽ dạy). App code chỉ thấy my_client.call_tool(...), không thấy session trực tiếp.

config = load_config()
client = MCPClient.from_config(config)  # chọn transport runtime

Mẹo nâng cao

Mẹo 1: "Tool search" thay vì list_tools

Nếu MCP server expose 50+ tool, flood context. Expose meta-tool search_tools(query) — model tìm tool cần thay vì nhận hết vào context. Anthropic đã ship pattern này trong API.

Mẹo 2: Tool namespacing

Khi connect nhiều MCP server có tool trùng tên (get_project ở cả Linear và Asana), prefix tool name với server ID: linear_get_project, asana_get_project. Model chọn đúng hơn.

Mẹo 3: Sampling — MCP đệ quy

Primitive nâng cao: MCP server có thể yêu cầu model completion thông qua client. Server gửi SamplingRequest, client dùng Anthropic API của user (không phải của server) để hoàn thành. Enable "little MCP agents" mà không cần mỗi server tự hold API key.

Áp dụng ngay

Bài tập 1: Vẽ lại flow (~10 phút)

Bước 1: Chọn 1 use case từ danh sách bạn viết ở Bài 7.0 (ví dụ: "Summarize Jira tickets").

Bước 2: Vẽ sơ đồ 11 bước (như diagram ở trên) cho use case đó. Ghi rõ:

Bước 3: Ghi lại các câu hỏi nảy ra:

Giữ những câu hỏi này. Chúng sẽ được trả lời ở các bài tiếp theo.

Bài tập 2: Quiz transport (~5 phút)

Với mỗi scenario, chọn transport phù hợp:

(Đáp án: 1-stdio, 2-HTTP, 3-WebSocket/SSE, 4-stdio hoặc HTTP tùy setup)

  • User query là gì?
  • App của bạn gửi list_tools() khi nào?
  • Claude sẽ chọn tool nào?
  • MCP server gọi API gì?
  • Câu hỏi kỹ thuật 1: ___________
  • Câu hỏi kỹ thuật 2: ___________
  • Câu hỏi kỹ thuật 3: ___________
  • ☐ Công cụ CLI cho dev dùng local, cần đọc file hệ thống — Transport: _____
  • ☐ Server public cho mọi user của SaaS, đằng sau OAuth — Transport: _____
  • ☐ Real-time stream progress của deep research task kéo dài 30 phút — Transport: _____
  • ☐ Integration với MCP server internal có sẵn trong công ty, single-user dev — Transport: _____

Tóm tắt bài học

🎯 MCP Client là cầu nối protocol — không phải business logic, không phải UI. Nó dịch giữa API gọn ghẽ của ứng dụng (list_tools()) và message JSON-RPC chuẩn MCP.

🎯 Transport agnostic — cùng code ứng dụng chạy trên stdio (local), HTTP (remote), WebSocket (real-time). Xu hướng 2026 là remote HTTP + OAuth.

🎯 8 message types cốt lõi — List/Call Tools, List/Read Resources, List/Get Prompts (+ notifications). Hiểu 3 cặp này là đủ cho >90% use case.

🎯 11-bước flow end-to-end — User query → tool discovery → Claude decision → tool execution → external API → result back → Claude response. Mỗi bước có 1 responsibility rõ ràng.

🎯 Wrap SDK Session trong class của bạn — Đừng xài ClientSession trực tiếp trong app logic. Wrap để dễ cleanup, thêm retry/logging, và dễ migrate transport.

Tài liệu tham khảo
  • MCP specification — Transports
  • MCP specification — Messages
  • "MCP 201" talk — Code with Claude, 7/2025
  • Python SDK: github.com/modelcontextprotocol/python-sdk
Nội dung này có hữu ích không?