1. Vì sao cần handshake
Nếu các REST endpoint là những cánh cửa rời rạc mà bạn gõ qua URL, thì MCP giống hơn với một cuộc đối thoại liên tục trên một kênh. Client không chỉ gửi các request rời rạc; trước hết nó thiết lập một phiên. Handshake là khoảnh khắc làm quen ở đầu phiên này.
Trong MCP, khoảnh khắc này được thực hiện như một request đặc biệt initialize mà client gửi ngay sau khi thiết lập transport (STDIO, HTTP/stream, WebSocket — không quan trọng). Trong request đó, client thông báo: “Tôi nói theo phiên bản MCP này, đây là những gì tôi làm được, và đây là tôi là ai”. Server đáp lại: “Tôi hỗ trợ phiên bản này và các khả năng này, rất hân hạnh”.
Sau trao đổi thành công, client gửi notification notifications/initialized và chỉ sau đó mới bắt đầu cuộc sống “làm việc”: tools/list, resources/list, tools/call và các thứ hữu ích khác.
Nếu ví von, handshake của MCP giống như hợp đồng thuê trước khi bạn đưa server vào data center. Khi bạn chưa thống nhất các quy tắc (định dạng giao thức, các dịch vụ data center cung cấp, ai chịu phí) — thì chở server đến là vô nghĩa.
Từ góc độ thực tế, handshake giải quyết ba việc:
- Kiểm tra tương thích phiên bản giao thức.
- Công bố các “kiến trúc nguyên thủy” MCP mà server thực sự hỗ trợ: tools, resources, prompts, logging, notification, v.v.
- Cung cấp meta‑info về client và server — tên và phiên bản hiện thực.
2. Vòng đời kết nối MCP: handshake diễn ra ở đâu
Để bức tranh bớt trừu tượng, hãy xem một kịch bản (flow) kết nối điển hình, đã giản lược:
sequenceDiagram
participant C as Client (ChatGPT/Inspector)
participant S as Máy chủ MCP
C->>S: (1) Thiết lập transport (STDIO/HTTP-stream)
C->>S: (2) Request: "initialize"
S-->>C: (3) Result: "initialize" (capabilities, serverInfo)
C->>S: (4) Notification: "notifications/initialized"
C->>S: (5) Request: "tools/list" / "resources/list"
S-->>C: (6) Result: danh sách tools/resources
C->>S: (7) Request: "tools/call" và các request khác
Về mặt kỹ thuật, các bước trông như sau:
- Transport được thiết lập: ví dụ, ChatGPT khởi chạy server của bạn như một subprocess và kết nối STDIO, hoặc Inspector gửi HTTP/stream tới /mcp.
- Client gửi JSON-RPC request initialize.
- Server trả về JSON-RPC result với các trường protocolVersion, capabilities và serverInfo.
- Client gửi notification notifications/initialized — tín hiệu: “tôi đã đọc xong, có thể bắt đầu làm việc”.
- Client gọi các phương thức discovery (tools/list, resources/list, prompts/list) tùy thuộc những gì thấy trong capabilities của server.
- Server trả metadata của tools/resources/prompts.
- Tiếp theo là các request “làm việc”: tools/call, resources/read và các request khác.
Điều quan trọng là handshake chỉ là một lời gọi JSON-RPC bình thường initialize. Không có phép màu nào cả. Sau bài về định dạng thông điệp MCP, bạn đã biết cách phân tích các request như vậy; điểm khác là ở đây phương thức luôn chỉ có một và “đặc biệt”, và nó chạy đầu tiên.
3. Client gửi gì trong initialize
Hãy phân tích request initialize theo từng phần. Đây là ví dụ một request tối thiểu (đơn giản hóa cho bài giảng):
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"elicitation": {}
},
"clientInfo": {
"name": "chatgpt-gift-client",
"version": "2.3.0"
}
}
}
Ví dụ này gần với những gì được thể hiện trong tài liệu chính thức của MCP. Các trường chính trong params:
protocolVersion
Chuỗi phiên bản của đặc tả MCP, thường ở dạng ngày, ví dụ "2025-06-18". Đây không phải phiên bản ứng dụng của bạn, mà là phiên bản của chính giao thức. Client nói: “tôi mong đợi nói theo phiên bản MCP này”. Server trong phản hồi phải hoặc xác nhận, hoặc trả lỗi nếu không biết phiên bản đó.
Đây là cách bảo vệ khỏi tình huống “client nghĩ một đằng, server hiện thực một nẻo”. Nếu không tìm được phiên bản chung, tốt hơn là ngắt kết nối một cách minh bạch thay vì trao đổi các thông điệp không tương thích.
capabilities của client
Đối tượng mà trong đó client khai báo những khả năng MCP mà chính nó hỗ trợ. Ví dụ, client ChatGPT thường ghi khóa elicitation, báo hiệu rằng nó có thể xử lý các yêu cầu tới người dùng (đầu vào bổ sung, xác nhận, v.v.).
Ví dụ:
"capabilities": {
"elicitation": {},
"sampling": {}
}
Server có thể dùng thông tin này để hiểu những khả năng mở rộng nào của giao thức đáng để sử dụng. Chẳng hạn, elicitation có nghĩa là client (ChatGPT) có thể đặt câu hỏi làm rõ và yêu cầu dữ liệu bổ sung từ người dùng.
clientInfo
Meta‑info đơn giản: tên và phiên bản của client.
"clientInfo": {
"name": "ChatGPT",
"version": "2.0.0"
}
Với nhà phát triển server, đây là vàng cho log: bạn luôn có thể xem chính xác client nào đang kết nối — ChatGPT, MCP Inspector, client thử nghiệm của bạn và nó có số phiên bản nào.
4. Server trả lời gì: initialize result
Phản hồi cho initialize là một JSON-RPC result bình thường với cùng id, nhưng trong trường result chứa mô tả những gì server làm được.
Trong request, ta xem capabilities từ phía client — tức là những gì client tự hỗ trợ. Giờ hãy phân tích đối tượng soi gương trong phản hồi: capabilities của server, tức là server làm được gì. Sơ lược:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {},
"prompts": {},
"logging": {}
},
"serverInfo": {
"name": "gift-genius-backend",
"version": "0.1.0"
}
}
}
Bạn sẽ thấy cấu trúc tương tự trong mô tả chính thức của giao thức và/hoặc mô tả SDK. Các phần chính:
protocolVersion trong phản hồi
Server hoặc lặp lại phiên bản do client đề xuất, hoặc (về lý thuyết) có thể chọn một phiên bản chung khác nếu có nhiều phiên bản. Trong các hiện thực điển hình, đơn giản là xác nhận phiên bản của client nếu server hỗ trợ. Nếu không — server phải trả lỗi và ngừng trao đổi.
serverInfo
Meta‑info về server: tên, phiên bản.
"serverInfo": {
"name": "gift-genius-backend",
"version": "0.1.0"
}
Nghe có vẻ nhàm chán, nhưng chính dựa vào dữ liệu này bạn sẽ lọc và tìm trong log: “tại sao ChatGPT phiên bản X không thương lượng được với server của chúng ta phiên bản Y”.
capabilities của server
Đây là trường thú vị nhất. Ở đây server công bố những MCP primitive và extension mà nó hỗ trợ: liệu nó có thể xử lý tools/*, resources/*, prompts/*, có thể gửi notification về thay đổi danh sách, v.v.
Nếu trong capabilities không có mục tools, thì bất kỳ client hiện thực đúng nào cũng sẽ không gọi tools/list hoặc tools/call. Tương tự, thiếu resources nghĩa là client sẽ không gửi resources/list và resources/read.
Vì vậy capabilities là một “hợp đồng nhẹ”: “có thể và không thể làm gì với server này”.
5. Capabilities như “danh sách siêu năng lực”
Tiếp theo chúng ta chỉ quan tâm đến capabilities của server — đối tượng được trả về trong phản hồi initialize và xác định các MCP primitive mà server này hỗ trợ.
Hãy xem chi tiết cấu trúc của nó. Ví dụ (đơn giản hóa nhưng gần với đặc tả):
{
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"prompts": {
"listChanged": false
},
"logging": {}
}
Ví dụ như vậy được phân tích trong kiến trúc MCP chính thức. Giải mã theo từng phần.
Capabilities.tools
Sự có mặt của khóa tools nói rằng: server có thể trả lời các phương thức tools/list và tools/call. Nếu có thêm cờ listChanged: true, nghĩa là server trong tương lai có thể gửi notification tools/list_changed khi tập công cụ thay đổi.
Với ChatGPT, điều này hữu ích: có thể cache danh sách công cụ và khi nhận list_changed thì cập nhật mà không cần reconnect hoàn toàn.
Capabilities.resources
Mục resources công bố rằng server hỗ trợ làm việc với tài nguyên: resources/list, resources/read, đôi khi có tìm kiếm. Các cờ bên trong:
- subscribe: true — client có thể đăng ký theo dõi thay đổi tài nguyên (ví dụ cho live‑log hoặc cập nhật file).
- listChanged: true — server có thể gửi notification resources/list_changed nếu tài nguyên được thêm hoặc bị xóa.
Điều này đặc biệt quan trọng với các thư mục lớn hoặc dữ liệu “sống” thay đổi liên tục.
Capabilities.prompts
Nếu server đăng ký các prompt dựng sẵn (ví dụ, mẫu gọi model gắn với domain của bạn), thì trong capabilities sẽ có khóa prompts. Ở đó cũng có thể có cờ listChanged.
Khi thấy mục này, client hiểu rằng có sẵn phương thức prompts/list và có thể là prompts/get.
Capabilities.logging và các mục khác
Một số hiện thực server còn công bố logging — nghĩa là server có thể gửi log có cấu trúc cho client qua MCP, ví dụ để debug.
Ngoài ra có thể xuất hiện các mục khác (ví dụ sampling hoặc extension đặc thù). Quan trọng là giao thức được thiết kế để mở rộng ngay từ đầu: bạn có thể thêm các khóa mới vào capabilities, và các client cũ sẽ đơn giản là bỏ qua nếu không biết về chúng.
Insight
Thực nghiệm cho thấy ChatGPT App bỏ qua các thông điệp listChanged được gửi tới nó. Hiện tại khi viết ứng dụng bạn không thể công bố một tập tools rồi sau đó thêm hoặc bớt vài tool nữa, dù giao thức MCP cho phép.
Tại thời điểm viết khóa học này: ngay lúc đăng ký ứng dụng của bạn trong ChatGPT Store, ChatGPT sẽ hỏi danh sách tools và resources từ ứng dụng của bạn và cache chúng vĩnh viễn. Khả năng tình hình thay đổi trong năm 2026 là cao; khả năng thay đổi trong quý I năm 2026 là thấp.
6. Discovery sau handshake: cách lấy danh sách công cụ và tài nguyên
Handshake trả lời câu hỏi “server nói chung làm được gì”. Bước tiếp theo — gọi là discovery: client dùng các phương thức cụ thể để kéo chi tiết — có những công cụ nào, tài nguyên nào sẵn có, các prompt nào đã cài sẵn.
Để làm điều đó, dùng các phương thức discovery: như tools/list, resources/list, prompts/list. Trong tài liệu kiến trúc MCP cũng đề xuất trình bày như vậy: handshake → discovery → gọi tool.
Ví dụ request tools/list:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
Phản hồi của server chứa một mảng công cụ: tên, mô tả, JSON Schema của tham số và đôi khi metadata như danh mục hoặc icon.
Sau đó ChatGPT (hoặc client khác) sẽ cache danh sách này và trong khi đối thoại sẽ dùng nó để:
- chọn công cụ phù hợp cho yêu cầu người dùng;
- kiểm tra tên công cụ có tồn tại;
- validate tham số trước khi gửi tools/call.
Với tài nguyên cũng tương tự, chỉ là resources/list thường hỗ trợ phân trang bằng cursor để không phải kéo ngay hàng triệu bản ghi. Điều này cũng được mô tả trong đặc tả MCP và được xem là trường hợp điển hình cho các thư mục lớn.
7. Handshake và capabilities với ví dụ ứng dụng GiftGen của chúng ta
Trong các mô-đun trước, chúng ta đã xây dựng ứng dụng học tập giúp gợi ý quà tặng. Ta đã có widget, có công cụ suggest_gifts ở backend, có một dạng danh mục quà. Bây giờ hãy hình dung handshake cho MCP server gift-genius trông thế nào.
Ví dụ handshake cho GiftGen
Request từ client:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"elicitation": {}
},
"clientInfo": {
"name": "ChatGPT",
"version": "2.1.0"
}
}
}
Phản hồi từ server của chúng ta:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "listChanged": true },
"prompts": {},
"logging": {}
},
"serverInfo": {
"name": "gift-genius-backend",
"version": "0.2.0"
}
}
}
Về cơ bản, ta gần như lặp lại ví dụ trong kiến trúc MCP chính thức, chỉ chỉnh tên cho phù hợp ứng dụng của mình.
Client rút ra điều gì từ phản hồi này:
- Có các công cụ (tools), và danh sách có thể thay đổi động (listChanged: true).
- Có tài nguyên (danh mục quà của chúng ta, có thể lưu trong file hoặc DB).
- Có prompts (ví dụ, mẫu “Hãy soạn mô tả ngắn cho món quà dành cho người dùng N”).
- Server có thể gửi log (hữu ích cho inspector và debug).
Tiếp theo client gọi tools/list và thấy, ví dụ, công cụ sau:
{
"name": "suggest_gifts",
"description": "Gợi ý ý tưởng quà tặng theo hồ sơ người nhận.",
"inputSchema": {
"type": "object",
"properties": {
"age": { "type": "integer" },
"relationship": { "type": "string" },
"budget": { "type": "number" }
},
"required": ["age", "relationship"]
}
}
Và bây giờ, khi người dùng viết kiểu: “Gợi ý quà cho chị/em gái, 25 tuổi, ngân sách đến 50 đô la”, mô hình đã biết: có công cụ suggest_gifts với bộ tham số như vậy, có thể gọi qua tools/call.
8. SDK “che” handshake (nhưng vì sao vẫn cần hiểu nó)
Trong TypeScript SDK cho MCP (cái mà chúng ta sẽ dùng ở bài sau), toàn bộ chuyện initialize và notifications/initialized được ẩn trong phương thức connect. Mã ví dụ:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "gift-genius",
version: "1.0.0",
});
// Đăng ký tool — SDK sẽ tự cấu hình capabilities.tools dựa trên mục này
server.tool(
"suggest_gifts",
{
description: "Gợi ý ý tưởng quà tặng.",
inputSchema: {
type: "object",
properties: {
age: { type: "integer" },
relationship: { type: "string" },
budget: { type: "number" },
},
required: ["age", "relationship"],
},
},
async (input) => {
// ... logic chọn gợi ý quà tặng ...
return { suggestions: [] };
},
);
const transport = new StdioServerTransport();
// Ở đây SDK:
// 1) nhận initialize từ client,
// 2) trả lời với serverInfo và capabilities,
// 3) chờ notifications/initialized,
// 4) sau đó bắt đầu xử lý các lời gọi tools/*.
await server.connect(transport);
SDK tự động tổng hợp capabilities dựa trên những gì bạn đã đăng ký: nếu có ít nhất một server.tool(...), nó sẽ thêm mục tools vào capabilities. Nếu bạn đăng ký resources hoặc prompts, sẽ xuất hiện resources và prompts.
Hiểu handshake và capabilities không phải để bạn tự tay viết JSON (đừng làm thế), mà để:
- đọc log MCP và hiểu vì sao client “không thấy” tool của bạn;
- chẩn đoán không tương thích phiên bản giao thức;
- khi cần thì hiện thực server tùy biến hoặc transport không tiêu chuẩn.
9. Phiên bản giao thức và sự tiến hóa của khả năng
Trường protocolVersion trong handshake không phải để trang trí. Trong đặc tả MCP nhấn mạnh: đây là cách thỏa thuận phiên bản giao thức tương thích; nếu không tìm được phiên bản chung, tốt hơn nên kết thúc kết nối.
Kịch bản điển hình:
- Bạn triển khai MCP server trên production với SDK hiện thực MCP phiên bản "2025-06-18".
- Một thời gian sau có phiên bản MCP mới, bạn cập nhật client, nhưng server vẫn cũ.
- Client gửi protocolVersion: "2026-02-01", server không biết phiên bản đó và trả lỗi invalid protocol version (hoặc tương tự).
Thực tế cho thấy: lập trình viên thường phớt lờ trường này rồi ngạc nhiên vì sao kết nối không được thiết lập.
Cách đúng khi làm việc với phiên bản:
- Luôn biết SDK của bạn hỗ trợ phiên bản MCP nào (thường ghi trong tài liệu/ghi chú phát hành).
- Khi cập nhật SDK — cập nhật phiên bản giao thức một cách có chủ đích.
- Log và giám sát phải hiển thị rõ lỗi khởi tạo do không khớp protocolVersion.
Việc mở rộng khả năng qua capabilities cũng gắn với tiến hóa: tính năng mới của MCP được thêm như khóa mới trong capabilities. Client cũ bỏ qua, client mới có thể dùng. Đây là mẫu được mô tả trong tài liệu MCP như một cách hỗ trợ tương thích ngược.
10. Handshake dưới góc nhìn của ChatGPT và Inspector
ChatGPT làm gì khi kết nối MCP
Khi bạn ở Dev Mode gắn MCP server vào ChatGPT, nền tảng phía sau sẽ làm đại khái:
- Mở transport (thường là HTTP/stream tới /mcp).
- Gửi initialize với protocolVersion, capabilities và clientInfo (kiểu như “ChatGPT Enterprise, phiên bản bao nhiêu đó”).
- Nhận phản hồi, cache capabilities của server.
- Gọi tools/list, resources/list, prompts/list tùy những gì thấy trong capabilities.
- Trong lúc đối thoại, khi mô hình quyết định gọi công cụ, nó so với cache này: có tool đó không, schema tham số như thế nào, và gọi ra sao.
Nếu capabilities của server không chứa tools, ChatGPT thậm chí sẽ không cố gợi ý App của bạn như một công cụ. Nếu trong capabilities có resources nhưng không có cờ listChanged, ChatGPT có thể cache danh sách tài nguyên và không đợi notification về thay đổi.
Inspector và MCP Jam giúp debug như thế nào
Các công cụ như MCP Jam / MCP Inspector làm gần như y hệt: thiết lập kết nối, thực hiện handshake, hiển thị capabilities của server cho bạn, và cho phép gọi tools/list, tools/call và các thứ khác bằng tay.
Từ góc nhìn nhà phát triển, đây là must‑have:
- thấy ngay protocolVersion thực tế mà server trả;
- thấy ngay trong capabilities có tools, resources, prompts hay không;
- có thể hiểu vì sao ChatGPT không thấy công cụ (capabilities chưa công bố hoặc handshake chưa xong).
Trong bài cuối của mô-đun này bạn sẽ dùng những công cụ đó nhiều hơn, nhưng ngay bây giờ cũng hữu ích khi hiểu rằng chúng hoạt động chính xác dựa trên handshake mà ta đang phân tích.
11. Lỗi thường gặp khi làm việc với handshake và capabilities
Trên lý thuyết mọi thứ khá thẳng thắn, nhưng trên thực tế chính handshake và việc công bố capabilities thường là nguồn của những bug rất cơ bản — nhất là trong Dev Mode hoặc MCP Inspector. Dưới đây là một vài lỗi thường gặp mà gần như chắc bạn sẽ gặp — trong code của mình hoặc trong log của đồng đội.
Lỗi #1: Sai định dạng request initialize.
Vấn đề rất hay gặp khi tự hiện thực MCP server không dùng SDK — là bỏ sót một trường bắt buộc của JSON-RPC. Ví dụ quên jsonrpc: "2.0", nhầm method (viết "init" thay vì "initialize"), hoặc biến capabilities thành giá trị boolean thay vì object. Đặc tả MCP yêu cầu định dạng chính xác; bất kỳ sai lệch nào cũng dẫn đến lỗi parse và ngắt kết nối. Tài liệu và hướng dẫn thực hành đều khuyên: trước hết hãy chắc rằng initialize của bạn bám sát đặc tả, rồi mới xem thứ khác.
Lỗi #2: Bỏ qua protocolVersion.
Đôi khi lập trình viên chỉ copy ví dụ từ tài liệu và đặt một chuỗi tùy ý, không xem SDK có hỗ trợ không. Kết quả là client và server nói theo hai phiên bản MCP khác nhau và kết nối không được thiết lập. Lỗi có thể ngụy trang thành “client không kết nối được”. Cần coi protocolVersion như một hợp đồng thật sự: phải thống nhất phiên bản này giữa đội frontend/nền tảng agent và đội viết MCP server.
Lỗi #3: Quên capabilities.
Tình huống kinh điển: bạn đã đăng ký tool trên server, nhưng khi tự hiện thực handshake lại quên thêm "tools": {} vào capabilities trong phản hồi initialize. Trong inspector bạn thấy có tool, còn ChatGPT lại hiển thị “No tools available” — vì nó tin capabilities và sẽ không gọi tools/list nếu thiếu mục tools. Các hướng dẫn troubleshooting cho Apps SDK cũng nhấn mạnh: nếu ChatGPT không thấy tool, việc đầu tiên là kiểm tra capabilities.
Lỗi #4: Cố dùng các phương thức không được công bố trong capabilities.
Đôi khi học viên thử nghiệm và, ví dụ, gửi resources/list tới một server mà trong capabilities không có mục resources. Về hình thức server có thể trả Method not found, nhưng đúng đắn hơn là không gọi các phương thức đó. MCP đưa vào capabilities chính là để bảo vệ khỏi các thử nghiệm như vậy. Client phải xem trước có mục tương ứng trong capabilities hay không rồi mới gọi phương thức.
Lỗi #5: Server bắt đầu “nói nhiều” trước notifications/initialized.
Nếu ngay sau khi trả lời initialize, server bắt đầu gửi log hoặc notification mà chưa đợi notifications/initialized, một số client có thể bỏ qua các thông điệp này hoặc thậm chí ngắt kết nối. Trong kiến trúc MCP chính thức nhấn mạnh rằng handshake phải kết thúc trước, và chỉ sau notification đã initialized mới bắt đầu “cuộc sống làm việc”.
Lỗi #6: Thay đổi schema của tool mà không báo hiệu danh sách thay đổi.
Khi bạn đổi JSON Schema của tool (biến một trường thành bắt buộc, đổi tên tham số), nhưng không khởi động lại server hoặc không gửi notification rằng danh sách tool đã thay đổi, cache của client có thể chứa phiên bản schema cũ. Điều này dẫn đến lỗi validate kỳ lạ. Đặc tả đề xuất dùng cờ listChanged và các notification tools/list_changed và resources/list_changed để giúp client cập nhật cache kịp thời.
Lỗi #7: Tối ưu sớm và “phép màu” quanh capabilities.
Đôi khi lập trình viên nghĩ ra các sơ đồ phức tạp với generate capabilities động, version theo client và đủ loại kỳ lạ khác khi chưa nắm cơ chế cơ bản. Lúc bắt đầu, chỉ cần công bố trung thực server làm được gì: tools, resources, prompts, logging. Mở rộng capabilities nên làm khi thực sự cần, không phải “để dành cho tương lai”. Đây thiên về phản mẫu tổ chức hơn là lỗi thuần giao thức, nhưng rất thường gặp trong dự án thực chiến.
GO TO FULL VERSION