Viết một script Python đơn giản chạy qua stdio để demo Model Context Protocol (MCP) trên máy tính cá nhân thì rất dễ. Nhưng để deploy một MCP Server vào Kubernetes cluster, phục vụ hàng nghìn AI Agent request mỗi phút mà không sập nguồn, chúng ta cần một ngôn ngữ biên dịch mạnh mẽ, memory footprint nhỏ gọn, và khả năng hỗ trợ concurrency (đồng thời) tuyệt vời. Đó là lý do Go (Golang) trở thành lựa chọn hàng đầu của các Infrastructure và Platform teams.

Trong bài viết này, chúng ta sẽ đi sâu vào việc sử dụng Go SDK để xây dựng một Production MCP Server, đồng thời tránh những cạm bẫy mà các kỹ sư mới làm quen với Agentic AI thường mắc phải.

1. Ba Nguyên Tắc Thiết Kế (Design Principles)

Trước khi gõ dòng code đầu tiên, các Tech Lead cần phải thống nhất 3 nguyên tắc sống còn khi thiết kế MCP Tools trong tổ chức:

  1. Bounded Context (Giới hạn ngữ cảnh): Đừng nhét mọi tool của toàn bộ hệ thống vào chung một “Super Server”. Hãy thiết kế server theo triết lý Domain-Driven Design (DDD). Ví dụ: mcp-billing-server chỉ xử lý các nghiệp vụ thanh toán, mcp-k8s-server chỉ tương tác với hạ tầng cluster. Việc chia nhỏ này giúp khoanh vùng rủi ro bảo mật (blast radius).
  2. Outcome-Oriented (Hướng kết quả): Agent không giống một giao diện frontend thông thường. Đừng phơi bày (expose) các API dạng CRUD cấp thấp như create_user_record, assign_user_role, send_welcome_email. Hãy expose API theo các luồng công việc (workflows) hoàn chỉnh: onboard_employee(email, department). Việc bắt LLM phải gọi quá nhiều tool nhỏ lẻ liên tiếp sẽ làm phình Context Window của nó, đốt tiền token, và tăng mạnh tỷ lệ sinh ra Hallucination.
  3. Stateless (Phi trạng thái): MCP Server tuyệt đối không được phép giữ state (trạng thái) cục bộ trong bộ nhớ RAM (in-memory). Mọi session state, transaction locks, hay caching phải được đẩy ra các hệ thống external storage như Redis hoặc PostgreSQL. Chỉ có như vậy, hệ thống mới đảm bảo an toàn khi Horizontal Pod Autoscaling (HPA) nhân bản server lên hàng chục instances. Điều này đã được nhắc đi nhắc lại trong loạt bài Agentic System Architecture.

2. Lựa Chọn SDK: Official vs Community

Trong hệ sinh thái Go cho MCP, lịch sử phát triển đã tạo ra một sự phân nhánh đáng lưu ý. Kỹ sư cần phân biệt rõ hai thư viện chính:

  • Community SDK (github.com/mark3labs/mcp-go): Trước khi dự án chính thức ra mắt SDK riêng, đây là thư viện cộng đồng phổ biến nhất. Nó cực kỳ linh hoạt và là người tiên phong hỗ trợ đa dạng các Transport Layers (bao gồm HTTP và SSE). Tuy nhiên, về lâu dài, nó phụ thuộc vào nỗ lực maintain của cộng đồng mở.
  • Official SDK (github.com/modelcontextprotocol/go-sdk): Bắt đầu từ 2025-2026, dự án MCP kết hợp với Google đã tung ra SDK chính thức. Nó tuân thủ nghiêm ngặt nhất mọi thay đổi của Protocol Specification, tối ưu cực tốt việc tự động sinh JSON Schema từ Go struct tags, và có tính ổn định enterprise.

Trong môi trường Enterprise sản xuất, chúng ta bắt buộc/khuyến nghị sử dụng Official SDK để đảm bảo khả năng tương thích dài hạn và nhận được các bản vá bảo mật nhanh nhất.

Khởi Tạo Project và go.mod

Hãy bắt đầu bằng việc khởi tạo Go module và tải dependencies. Mở terminal và chạy:

mkdir cloud-ops-mcp
cd cloud-ops-mcp
go mod init my-mcp-server
go get github.com/modelcontextprotocol/go-sdk/mcp

File go.mod của bạn sẽ trông giống thế này:

module my-mcp-server

go 1.23.0 // Hoặc phiên bản Go mới nhất

require (
	github.com/modelcontextprotocol/go-sdk v1.x.x
)

3. Cấu trúc Code và Schema Validation

LLM rất nhạy cảm với tên tham số (parameters) và mô tả (descriptions). Schema của bạn càng lỏng lẻo, LLM càng dễ “ảo giác” (hallucinate) ra các tham số không có thực hoặc truyền sai định dạng. Trong Go, chúng ta sẽ tận dụng triệt để sức mạnh của struct tags kết hợp với jsonschema để official SDK tự động sinh (gen) schema hoàn hảo cho LLM.

Hãy xem ví dụ định nghĩa một công cụ cấp phép tài nguyên đám mây (provision_cloud_resource):

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

// Khai báo Schema khắt khe với jsonschema tags
type ProvisionRequest struct {
	ResourceType string `json:"resource_type" jsonschema:"required,enum=ec2,enum=s3,description=Loại tài nguyên cần cấp phát (chỉ nhận 'ec2' hoặc 's3')"`
	Region       string `json:"region" jsonschema:"required,description=AWS Region (ví dụ: 'us-west-2')"`
	RequestID    string `json:"request_id" jsonschema:"required,description=UUID do client sinh ra để đảm bảo Idempotency. Bắt buộc phải có."`
}

func main() {
	// Khởi tạo Server
	server := mcp.NewServer("cloud-ops-mcp", "1.0.0")

	// Đăng ký Tool với hệ thống
	mcp.AddTool(server, mcp.Tool{
		Name:        "provision_cloud_resource",
		Description: "Công cụ tạo mới một tài nguyên cloud. Agent bắt buộc phải gọi hàm này trước khi thực hiện deploy source code lên môi trường.",
	}, handleProvision)

	// Khởi chạy Server. 
	// (Trong Production thực tế chúng ta sẽ dùng Streamable HTTP transport. Ở đây dùng Stdio để minh họa luồng cơ bản)
	log.SetOutput(os.Stderr)
	log.Println("Đang khởi chạy Model Context Protocol Server...")

	if err := server.ServeStdio(); err != nil {
		log.Fatalf("Server crash: %v", err)
	}
}

Tại sao lại cần RequestID (Idempotency)?

Môi trường mạng và Agentic Workflows không bao giờ hoàn hảo. Network có thể rớt gói tin (packet loss), mô hình LLM API có thể bị timeout. Khi đó, framework của Agent thường được thiết kế để tự động retry (thử lại) lời gọi hàm.

Nếu bạn không thiết kế tool có tính chất Idempotency (nghĩa là dù gọi 1 lần hay 100 lần liên tiếp với cùng một parameter, kết quả hệ thống vẫn không thay đổi), việc retry có thể khiến Agent vô tình tạo ra 5 cái EC2 instances thay vì 1 cái. Hóa đơn cloud cuối tháng sẽ là một thảm họa.

Luôn luôn yêu cầu Agent tự sinh ra một request_id (hoặc idempotency_key). Server Go của bạn sẽ lưu cache key này (trong Redis) để de-duplicate (khử trùng lặp) các request đến sau. Điều này cũng đã được nhắc trong cẩm nang AI Driven Playbook.

4. Xử Lý Logic và Trả Về Kết Quả (Result Handling)

Khi Agent gọi tool, kết quả trả về phải là một thông điệp (text hoặc JSON cấu trúc) mà LLM có thể đọc, parse và hiểu được một cách dễ dàng. Tuyệt đối không trả về raw HTML, binary dump, hay một stack trace rác.

func handleProvision(ctx context.Context, req mcp.CallToolRequest) (mcp.CallToolResult, error) {
	// Parse arguments (Official SDK tự động map JSON vào map hoặc struct tùy hàm hỗ trợ)
	args := req.Arguments
	reqID, _ := args["request_id"].(string)
	resourceType, _ := args["resource_type"].(string)
	region, _ := args["region"].(string)

	// Validate logic cơ bản
	if reqID == "" {
		return mcp.NewToolResultError("Validation Failed: request_id is missing"), nil
	}

	// TODO: Kiểm tra Redis/Database xem request_id này đã được xử lý chưa (Idempotency Check)
	
	// Thực thi business logic (ví dụ: gọi AWS SDK để tạo EC2)
	log.Printf("[req: %s] Bắt đầu cấp phát %s tại %s", reqID, resourceType, region)

	// Trả kết quả thành công dưới dạng Text tường minh cho LLM
	msg := fmt.Sprintf("✅ Tác vụ hoàn tất. Tài nguyên %s đã được cấp phát thành công tại khu vực %s. Resource ARN tham chiếu: arn:aws:%s:12345:res-01.", 
		resourceType, region, resourceType)

	return mcp.NewToolResultText(msg), nil
}

5. Cạm Bẫy Chết Người: Ghi Log Vào STDOUT

Đây là lỗi sơ đẳng nhưng lại phổ biến nhất của các development team khi mới chuyển từ việc viết REST backend sang viết MCP Server qua stdio: Họ dùng fmt.Println() để in log debug.

Giao thức MCP (khi chạy transport qua stdio) truyền tải các JSON-RPC packets tiêu chuẩn qua luồng stdout. Bất kỳ một ký tự nào, một dòng log thừa nào không phải là chuẩn JSON-RPC bị in ra stdout sẽ ngay lập tức làm Client (như Claude Desktop hay Agent Gateway) bị crash do lỗi parse JSON.

Quy tắc vàng bằng máu: Trong code MCP Server, mọi logs, warnings, errors nội bộ bắt buộc phải được định tuyến vào stderr (như dòng log.SetOutput(os.Stderr) ở trên) hoặc được gửi đi thông qua một hệ thống telemetry chuyên dụng (như OpenTelemetry).

Lời Kết

Bạn vừa đi qua cấu trúc nền tảng của một Production MCP Server được viết bằng Golang. Để đứng vững trên môi trường Enterprise, nó cần một Schema định nghĩa khắt khe, tính năng Idempotency by-design, và kỷ luật Logging nghiêm ngặt để không làm đứt gãy giao thức.

Nhưng làm sao để bảo vệ server này? Điều gì sẽ ngăn cản một con AI Agent xấu tính (hoặc bị hack) tự ý gọi provision_cloud_resource và tạo ra hàng nghìn máy ảo lớn đốt sạch tài khoản ngân hàng của công ty? Chúng ta sẽ cần giải quyết triệt để bài toán định danh và phân quyền.


Next up: Phần 3: Identity & AuthN for Agents