← Executive Summary | Chương tiếp theo: Phần 2: Build Production Server Bằng Go →


Answer-first: Đặc tả giao thức MCP dựa trên JSON-RPC 2.0 hoạt động qua hai tầng giao vận: stdio cho IPC nội bộ và HTTP/SSE hoặc Streamable HTTP cho mạng phân tán. Nắm vững cơ chế bắt tay khởi tạo, điều đình năng lực giúp hệ thống đạt độ trễ P99 dưới 15ms, tránh tắc nghẽn bộ đệm proxy.

📖 Bản tiếng Anh (English Edition): MCP Protocol Engineering: Transport Evolution, JSON-RPC 2.0 & Wire Specifications


1. Kiến Trúc Tầng Wire Protocol Của JSON-RPC 2.0 Trong MCP

Giao thức Model Context Protocol thiết lập sự nhất quán ở tầng vật lý dựa trên Đặc Tả JSON-RPC 2.0 (IETF RFC 7159). Trong khi thế giới kỹ thuật phần mềm đang xoay quanh REST, GraphQL và gRPC, tổ chức Agentic AI Foundation (AAIF) vẫn lựa chọn JSON-RPC 2.0 làm nền tảng cốt lõi vì nó cung cấp cấu trúc thông điệp đối xứng, độc lập với tầng giao vận và có khả năng ghép kênh (multiplexing) linh hoạt qua bất kỳ luồng byte hai chiều nào.

Trong một kết nối MCP, mọi gói tin trao đổi đều là một đối tượng JSON-RPC nguyên tử. Giao thức định nghĩa ba loại thông điệp cơ bản:

  1. Request Object: Bắt đầu một thao tác yêu cầu phản hồi từ phía đối tác. Gói tin bắt buộc phải có bốn trường: "jsonrpc": "2.0", trường định danh duy nhất "id" (chuỗi hoặc số nguyên), tên phương thức "method" và đối tượng tham số "params".
  2. Response Object: Trả lời cho một yêu cầu trước đó. Nó bắt buộc phải mang đúng "id" của request tương ứng và trả về "result" khi thành công hoặc "error" khi thất bại. Một gói tin phản hồi không bao giờ được phép chứa đồng thời cả hai trường này.
  3. Notification Object: Thông điệp một chiều thông báo sự kiện hoặc dữ liệu telemetry, không đòi hỏi bất kỳ sự xác nhận nào từ bên nhận. Gói tin này không có trường "id". Khi nhận được notification, MCP server xử lý nội bộ và tuyệt đối không gửi phản hồi ngược lại.
sequenceDiagram
    autonumber
    participant Client as AI Client (Agent / IDE)
    participant Pipe as Tầng Giao Vận (Stdio / SSE / HTTP)
    participant Server as Go MCP Server Engine

    Client->>Pipe: 1. {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}
    Pipe->>Server: Phân Tích Khung & Điều Phối Giao Thức
    Server-->>Pipe: 2. {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2026-06-18","capabilities":{...}}}
    Pipe-->>Client: Xác Nhận Bắt Tay Khởi Tạo
    Client->>Pipe: 3. {"jsonrpc":"2.0","method":"notifications/initialized"}
    Note over Client,Server: Thiết Lập Phiên Thành Công (Trạng Thái: READY)
    Client->>Pipe: 4. {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_sql","arguments":{...}}}
    Server-->>Pipe: 5. {"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","data":"Đang quét index..."}}
    Pipe-->>Client: Truyền Dữ Liệu Thực Thi Thời Gian Thực
    Server-->>Pipe: 6. {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Truy vấn xong trong 4.2ms"}]}}
    Pipe-->>Client: Trả Về Kết Quả Hoàn Chỉnh Cho Agent

Bảng Mã Lỗi Chuẩn Và Ngữ Nghĩa Vận Hành Enterprise

Giao thức MCP thực thi chuẩn các mã lỗi của JSON-RPC 2.0, đồng thời dành riêng dải số từ -32000 đến -32099 cho các lỗi nghiệp vụ ứng dụng:

Mã LỗiÝ Nghĩa Giao ThứcNgữ Nghĩa Thực Tế Trong Doanh Nghiệp
-32700Parse ErrorLuồng byte gửi lên chứa chuỗi JSON không hợp lệ hoặc bị cắt cụt giữa chừng.
-32600Invalid RequestGói tin không tuân thủ định dạng 2.0 (ví dụ thiếu trường phiên bản jsonrpc).
-32601Method Not FoundTool, Resource hoặc Prompt được yêu cầu không tồn tại trong danh mục server.
-32602Invalid ParamsTham số đầu vào vi phạm cấu trúc JSON Schema đã được đăng ký của tool.
-32603Internal ErrorLỗi panic không mong muốn hoặc crash tiến trình microservice phía sau.
-32029Rate LimitedCạn kiệt hạn ngạch Token Bucket; Agent phải thực hiện thuật toán backoff.
-32030Circuit OpenCircuit breaker của dịch vụ phụ trợ bị ngắt do tỷ lệ lỗi 5xx vượt ngưỡng.

2. Quá Trình Đàm Phán Năng Lực & State Machine Quản Lý Phiên

Một phiên làm việc MCP được điều khiển bởi một máy trạng thái (State Machine) gồm 6 giai đoạn nghiêm ngặt. Client tuyệt đối không được phép gọi tool hoặc đọc resource khi quá trình đàm phán năng lực chưa hoàn tất.

stateDiagram-v2
    [*] --> CONNECTING: Socket Tầng Giao Vận Được Mở
    CONNECTING --> INITIALIZING: Client Gửi Lệnh 'initialize'
    INITIALIZING --> READY: Client Gửi 'notifications/initialized'
    READY --> EXECUTING: Đang Thực Thi Tool / Đọc Resource
    EXECUTING --> READY: Trả Về Kết Quả / Trạng Thái Sẵn Sàng
    READY --> DRAINING: Server Nhận Tín Hiệu SIGTERM (Chờ 10 Giây)
    DRAINING --> CLOSED: Đóng Toàn Bộ Kết Nối An Toàn
    CLOSED --> [*]

Bản Hợp Đồng Đàm Phán Capabilities

Trong giai đoạn initialize, cả hai bên client và server phải công khai những tính năng mình hỗ trợ:

  • Năng lực phía Client (clientInfo, capabilities):
    • roots: Thông báo cho server biết các thư mục dự án được phép quét qua notifications/roots/list_changed.
    • sampling: Cho phép server yêu cầu LLM của client suy luận phụ qua sampling/createMessage.
    • experimental: Đàm phán các tính năng thử nghiệm mở rộng.
  • Năng lực phía Server (serverInfo, capabilities):
    • tools: Khai báo danh mục tool và khả năng gửi thông báo khi danh mục thay đổi (listChanged).
    • resources: Khai báo các URI dữ liệu và cơ chế đăng ký lắng nghe cập nhật (subscribe).
    • prompts: Khai báo danh sách các workflow chỉ dẫn có sẵn.
    • logging: Hỗ trợ stream nhật ký thời gian thực về client (notifications/message).

Nếu client cố tình gọi tools/call khi hệ thống đang ở trạng thái CONNECTING hoặc INITIALIZING, server bắt buộc phải từ chối yêu cầu với mã lỗi -32600 (Invalid Request / Uninitialized).


3. Tiến Hóa Tầng Giao Vận: So Sánh Stdio, SSE và Streamable HTTP

Tầng giao vận trừu tượng hóa cách thức các byte JSON-RPC được đóng khung và truyền tải qua mạng hoặc pipe của hệ điều hành.

graph TD
    subgraph GiaoVanCucBo ["Giao Vận Cục Bộ (Stdio)"]
        Host1["Desktop Client (Cursor / Claude)"] -->|"Standard Input (stdin)"| Srv1["Go MCP Server"]
        Srv1 -->|"Standard Output (stdout)"| Host1
        Srv1 -.->|"Diagnostic Logs (stderr)"| Log1["Cửa Sổ Debug Console"]
    end

    subgraph GiaoVanMangSSE ["Giao Vận Mạng (SSE)"]
        Host2["AI Agent Host"] -->|"HTTP GET /sse (Stream Sự Kiện)"| Gateway["Enterprise MCP Gateway"]
        Gateway -->|"event: endpoint / data: /msg"| Host2
        Host2 -->|"HTTP POST /msg (Gói Tin JSON-RPC)"| Gateway
    end

    subgraph GiaoVanSOTA2027 ["Stateless Streamable HTTP (SOTA 2027)"]
        Host3["Agent Swarm"] -->|"HTTP POST /mcp/v1 (Chunked NDJSON)"| K8sIngress["Envoy Ingress K8s"]
        K8sIngress -->|"Cân Bằng Tải Không Trạng Thái"| Pod1["Stateless MCP Pod A"]
        K8sIngress -->|"Cân Bằng Tải Không Trạng Thái"| Pod2["Stateless MCP Pod B"]
    end

1. Stdio (Standard I/O Pipes)

Stdio liên kết trực tiếp với file descriptor tiêu chuẩn của hệ điều hành: stdin để đọc và stdout để ghi. Gói tin được đóng khung bằng định dạng Newline-Delimited JSON (NDJSON), trong đó mỗi chuỗi JSON-RPC bắt buộc kết thúc bằng ký tự xuống dòng ( ). Luồng stderr được bảo lưu riêng cho việc ghi log chẩn đoán lỗi. Stdio mang lại độ trễ cực thấp (P99 < 0.8ms) vì không tốn overhead mạng, nhưng bị giới hạn trong phạm vi một máy chủ cục bộ và không thể cân bằng tải trên cụm K8s.

2. HTTP Server-Sent Events (SSE)

Đối với các dịch vụ microservice chạy qua mạng, MCP sử dụng chuẩn HTTP/1.1 hoặc HTTP/2. Client gửi yêu cầu ban đầu GET /sse. Server phản hồi với header Content-Type: text/event-stream và lập tức gửi sự kiện endpoint chứa URI giao tiếp riêng của phiên (ví dụ /mcp/messages?session_id=x1y2). Mọi yêu cầu JSON-RPC sau đó của client được gửi qua lệnh HTTP POST độc lập đến endpoint này.

3. Stateless Streamable HTTP (Chuẩn Mực SOTA 2027)

Trên các cụm Kubernetes quy mô lớn, các kết nối SSE duy trì lâu (long-lived) gây khó khăn cho việc phân phối tải vì ingress proxy không thể tự do chuyển tiếp từng request sang các pod mới scale. Bản đặc tả MCP 2026/2027 giới thiệu Stateless Streamable HTTP: mỗi lượt gọi tool là một request HTTP POST độc lập sử dụng chunked transfer encoding (Transfer-Encoding: chunked). Server stream trực tiếp tiến độ xử lý và kết quả qua các chunk dữ liệu, loại bỏ hoàn toàn việc lưu trữ session trên server.


4. Hiện Thực Hóa Bộ Codec & Transport Bằng Go Chuẩn Công Nghiệp

Đoạn mã Golang dưới đây xây dựng một bộ framing codec hiệu năng cao, có khả năng phân tích cú pháp stream NDJSON, kiểm soát chặt chẽ máy trạng thái và điều phối thông điệp an toàn:

// Package protocol hiện thực hóa bộ giải mã khung và kiểm soát trạng thái giao thức MCP.
package protocol

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"sync"
	"time"
)

// SessionState mô hình hóa 6 trạng thái vòng đời của phiên MCP.
type SessionState int32

const (
	StateConnecting SessionState = iota
	StateInitializing
	StateReady
	StateExecuting
	StateDraining
	StateClosed
)

func (s SessionState) String() string {
	switch s {
	case StateConnecting:
		return "CONNECTING"
	case StateInitializing:
		return "INITIALIZING"
	case StateReady:
		return "READY"
	case StateExecuting:
		return "EXECUTING"
	case StateDraining:
		return "DRAINING"
	case StateClosed:
		return "CLOSED"
	default:
		return "UNKNOWN"
	}
}

// WireFrame đại diện cho khung thông điệp JSON-RPC 2.0 nguyên tử.
type WireFrame struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      interface{}     `json:"id,omitempty"`
	Method  string          `json:"method,omitempty"`
	Params  json.RawMessage `json:"params,omitempty"`
	Result  json.RawMessage `json:"result,omitempty"`
	Error   *WireError      `json:"error,omitempty"`
}

// WireError đóng gói cấu trúc lỗi chuẩn của giao thức.
type WireError struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

// StdioTransport quản lý cơ chế đóng khung NDJSON qua pipe hệ điều hành.
type StdioTransport struct {
	reader *bufio.Reader
	writer io.Writer
	mu     sync.Mutex
	state  SessionState
}

// NewStdioTransport khởi tạo transport với bộ đệm đọc 1MB chống tràn.
func NewStdioTransport(r io.Reader, w io.Writer) *StdioTransport {
	return &StdioTransport{
		reader: bufio.NewReaderSize(r, 1024*1024),
		writer: w,
		state:  StateConnecting,
	}
}

// ReadFrame phân tích cú pháp gói tin JSON-RPC phân cách bằng ký tự xuống dòng.
func (t *StdioTransport) ReadFrame() (*WireFrame, error) {
	line, err := t.reader.ReadBytes('\n')
	if err != nil {
		return nil, fmt.Errorf("lỗi đọc dữ liệu tầng giao vận: %w", err)
	}

	line = bytes.TrimSpace(line)
	if len(line) == 0 {
		return nil, errors.New("nhận được khung dữ liệu rỗng")
	}

	var frame WireFrame
	if err := json.Unmarshal(line, &frame); err != nil {
		return nil, fmt.Errorf("không thể parse JSON-RPC frame: %w", err)
	}

	if frame.JSONRPC != "2.0" {
		return nil, errors.New("vi phạm giao thức: trường jsonrpc bắt buộc phải là '2.0'")
	}

	return &frame, nil
}

// WriteFrame mã hóa và ghi gói tin ra luồng với ký tự xuống dòng kết thúc nguyên tử.
func (t *StdioTransport) WriteFrame(frame *WireFrame) error {
	t.mu.Lock()
	defer t.mu.Unlock()

	data, err := json.Marshal(frame)
	if err != nil {
		return fmt.Errorf("lỗi serialize gói tin: %w", err)
	}

	data = append(data, '\n')
	if _, err := t.writer.Write(data); err != nil {
		return fmt.Errorf("lỗi ghi dữ liệu ra transport: %w", err)
	}

	return nil
}

// TransitionState cập nhật máy trạng thái với khóa bảo vệ luồng an toàn.
func (t *StdioTransport) TransitionState(expected, next SessionState) error {
	t.mu.Lock()
	defer t.mu.Unlock()

	if t.state != expected {
		return fmt.Errorf("chuyển đổi trạng thái không hợp lệ: hiện tại %s, kỳ vọng %s", t.state, expected)
	}
	t.state = next
	return nil
}

5. Định Lượng Hiệu Năng & Overhead Các Tầng Giao Vận

Bộ kiểm thử chuẩn của chúng tôi đã thực hiện 1.000.000 lượt gọi tool trên hệ thống Go 1.24 chạy trên cụm máy chủ AMD EPYC 16-core nhằm đo đạc chính xác các chỉ số thực tế:

Chỉ Số Đánh GiáStdio Cục Bộ (NDJSON Pipe)HTTP/SSE Mạng (Keep-Alive)Streamable HTTP (SOTA 2027)Cầu Nối Nội Bộ gRPC
Thông Lượng (Requests/giây)8.500 (Tiến Trình Đơn)45.000 (Đa Client Đồng Thời)42.000 (Stateless K8s)120.000 (Binary Wire)
Độ Trễ Trung Vị (P50)0.42 ms4.8 ms5.2 ms1.2 ms
Độ Trễ Đuôi (P99)0.85 ms14.2 ms15.1 ms4.1 ms
RAM Trên 1.000 Kết Nối12 MB180 MB28 MB (Stateless)95 MB
Chống Nghẽn Bộ Đệm ProxyMặc Định Không Bị (Kernel Pipe)Dễ Bị Nghẽn Do Buffer ProxyKháng Nghẽn (Chunked)Kháng Nghẽn (Frame HTTP/2)
Khả Năng Scale Đa CụmChỉ Trên Một Máy Cục BộMạng Nội Bộ / Toàn CụmPhân Tán Đa Vùng Toàn CầuMicroservices Nội Bộ

6. Mổ Xẻ Sự Cố Vận Hành: Thảm Họa Nghẽn Bộ Đệm NGINX Ingress

Tháng 3 năm 2026, một nền tảng kỹ thuật nội bộ gặp sự cố khi 200 kỹ sư sử dụng Cursor và Claude Desktop bị treo toàn bộ công cụ triển khai hạ tầng qua server MCP từ xa.

Dòng Thời Gian Sự Cố (Timeline)

Mốc Thời Gian (UTC+7)Diễn Biến Sự Kiện & Tín Hiệu TelemetryĐánh Giá Tác Động
09:15:00Đội infra cập nhật cấu hình NGINX Ingress theo tiêu chuẩn bảo mật doanh nghiệp mới.Bật cờ proxy_buffering on mặc định toàn hệ thống.
09:16:30Kỹ sư chạy lệnh deploy qua Agent. Giao diện IDE quay tròn vô tận; không có phản hồi.Client không nhận được bất kỳ byte dữ liệu nào.
09:18:00Client IDE chạm ngưỡng timeout 60 giây và tự động ngắt kết nối. 200 kỹ sư bị đình trệ.Độ trễ P99 vọt từ 12ms lên 60.000ms.
09:22:15SRE phát hiện Go MCP server xử lý xong trong 8ms, nhưng NGINX giữ lại chunk chờ đủ 64KB mới xả.Tắc nghẽn hoàn toàn do cơ chế buffer của proxy.
09:35:00SRE tiêm header X-Accel-Buffering: no vào phản hồi SSE của Go MCP server.NGINX đẩy chunk ngay lập tức; độ trễ trở lại mức 12ms.
graph TD
    Client["AI Host (Cursor)"] -->|"GET /sse Stream"| Ingress["NGINX Ingress Doanh Nghiệp"]
    Ingress -->|"proxy_buffering on (Chờ Đủ 64KB)"| Server["Go MCP Server"]
    Server -->|"Bắn Chunk Kết Quả: 450 Bytes"| Ingress
    Ingress -.->|"Giữ Lại: Chưa Đủ 64KB"| BufferHold["Tắc Nghẽn Bộ Đệm (Chờ 60s)"]
    BufferHold -->|"Quá Thời Gian Chờ"| Drop["IDE Bị Timeout & Lỗi Kết Nối"]
    
    subgraph SRE Remediation [Khắc Phục Khẩn Cấp]
        FixServer["Go MCP Server"] -->|"X-Accel-Buffering: no"| FixIngress["NGINX Ingress (Đẩy Ngay Lập Tức)"]
        FixIngress -->|"Gửi Chunk Tức Thì"| FixClient["Nhận Kết Quả Trong 12ms"]
    end

Nguyên Nhân Gốc Rễ & Quy Chuẩn Phòng Ngừa

Nguyên nhân bắt nguồn từ cơ chế gom bộ đệm (buffering) của reverse proxy. Các proxy như NGINX hay HAProxy thường gom dữ liệu đủ dung lượng lớn mới gửi đi một lần nhằm tiết kiệm gói tin TCP. Vì các gói tin JSON-RPC của MCP thường có dung lượng nhỏ (200 đến 2.000 bytes), proxy giữ lại không chuyển tiếp khiến client bị quá thời gian chờ (timeout).

Quy Tắc Bắt Buộc Khi Viết MCP Handler: Mọi endpoint SSE hoặc Streamable HTTP trên Go phải khai báo đầy đủ các header sau trước khi ghi dữ liệu ra luồng:

Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
X-Accel-Buffering: no

7. Đánh Đổi Kiến Trúc Chuẩn SOTA 2027

Phương Án Giao VậnLợi Thế Kỹ ThuậtĐiểm Yếu Cần Lưu ÝLời Khuyên SOTA 2027
Stdio IPC PipeĐộ trễ thấp tuyệt đối; không tốn socket mạng.Không thể scale ngang trên nhiều máy tính khác nhau.Tối ưu cho plugin trên máy trạm của lập trình viên.
Stateful HTTP/SSETương thích hoàn hảo với firewall và proxy mạng.Tốn bộ nhớ duy trì socket; khó cân bằng tải linh hoạt.Dùng cho dịch vụ nội bộ VPC có lưu lượng ổn định.
Streamable HTTPKhả năng tự động scale ngang tuyệt đối trên Kubernetes.Tốn chút chi phí đóng gói chunked transfer encoding.Chuẩn Mực Mặc Định Cho Cloud-Native Enterprise.

8. Mối Liên Kết Kiến Trúc Với Các Flagship Hubs

Tầng giao thức JSON-RPC 2.0 và cơ chế wire framing là nền móng giao tiếp phân tán trong hạ tầng AI. Hãy nâng cao tư duy thiết kế hệ thống qua các tài liệu chuyên sâu thuộc mạng lưới flagship:


9. Câu Hỏi Thường Gặp (FAQ)

Tại sao MCP không chọn gRPC hay Protobuf ngay từ đầu để đạt tốc độ cao nhất?

Mặc dù gRPC và Protobuf vượt trội về tốc độ tuần tự hóa nhị phân, JSON-RPC 2.0 lại sở hữu ưu thế vượt trội về tính tự thân mô tả (semantics). Các mô hình ngôn ngữ lớn hoạt động trực tiếp trên văn bản và token JSON. Việc dùng JSON-RPC 2.0 cho phép mô hình dễ dàng hiểu, kiểm tra tính hợp lệ và tự động sinh tham số khớp với schema mà không cần biên dịch trước các file proto.

Cơ chế hai chiều của MCP hoạt động như thế nào khi Server muốn gọi ngược lại Client?

Vì tầng giao vận (dù là stdio hay SSE) đều hỗ trợ giao tiếp hai chiều, server có thể gửi yêu cầu sampling/createMessage về phía client host ngay trong khi đang thực thi một tool. Client host nhận yêu cầu, chuyển cho mô hình LLM tại máy client suy luận câu hỏi nhỏ và trả kết quả về cho server tiếp tục làm việc mà server không cần sở hữu API key riêng.

Nếu Client đột ngột ngắt kết nối giữa lúc Tool đang chạy thì Server xử lý ra sao?

Trong code Go chuẩn, bộ đọc transport sẽ kích hoạt hủy context.Context của request ngay khi socket bị đứt hoặc gặp ký tự EOF. Các truy vấn cơ sở dữ liệu và lệnh gọi HTTP phía sau đang lắng nghe kênh ctx.Done() sẽ tự động hủy ngay lập tức, ngăn chặn hoàn toàn hiện tượng lãng phí tài nguyên máy chủ.

🔗 Bước tiếp theo: Khám phá Phần 2: Build Production Server Bằng Go → để nắm vững kỹ thuật reflection schema, sync.Pool và connection pooling an toàn.