# MCP server là gì: mổ một MCP server đang chạy thật, từ 3 công cụ lên 9 và 18 KB mô tả trợ lý phải đọc

MCP server là gì, giải thích bằng một server đang chạy: hai đường truyền, danh sách công cụ trợ lý đọc, kết quả trả về, đăng nhập OAuth và ba chỗ đã làm sai.

Published: 2026-10-05 · Language: vi · Tags: MCP, AI agent, tích hợp, OAuth · Canonical: https://hecigo.com/blog/mcp-server-la-gi-nhin-tu-mot-server-dang-chay-that/

---

Ngày 12/09/2026, bản đầu của [Ads Insights](https://ads.hecigo.com/) có ba công cụ. Hơn ba tuần sau nó có chín, và danh sách công cụ mà trợ lý AI nhận về trước khi trả lời câu hỏi đầu tiên đo được **18.778 byte**. Riêng mô tả và schema tham số của một công cụ, `ads_insights`, đã chiếm hơn 6.600 ký tự. Người dùng không bao giờ thấy những dòng đó. Trợ lý thì đọc chúng trong mọi cuộc hội thoại, và quyết định gọi gì, truyền gì, cẩn thận tới đâu dựa trên chính chúng.

Đó là câu trả lời thực dụng nhất cho câu hỏi MCP server là gì: một chương trình mà phần lớn "giao diện" của nó là chữ viết cho máy đọc. Bài này đi qua một MCP server đang chạy thật của hecigo, từ đường truyền tới danh sách công cụ, kết quả trả về, đăng nhập, và ba chỗ đã làm sai.

## MCP server là gì

Model Context Protocol (MCP) là một giao thức dựa trên JSON-RPC 2.0. Trong đó có ba vai:

- **Host** là ứng dụng AI: Claude Desktop, Claude Code, VS Code, ChatGPT.
- **Client** là thành phần bên trong host, giữ đúng một kết nối tới một server. Host nối ba server thì tạo ba client.
- **Server** là chương trình cung cấp ngữ cảnh và hành động cho client, bất kể nó chạy trên máy bạn hay trên Internet.

Một MCP server có thể đưa ra ba loại thứ: **tools** (hàm trợ lý gọi được, ví dụ gọi một API), **resources** (dữ liệu để đọc làm ngữ cảnh) và **prompts** (khuôn câu dựng sẵn). Ads Insights chỉ dùng tools, vì mọi việc của nó là gọi API quảng cáo của Meta, Google và TikTok rồi trả số.

Câu hỏi đáng tự kiểm trước khi dựng một MCP server: việc của bạn là cho trợ lý *làm* (tools), cho trợ lý *đọc* (resources), hay cho người dùng một *câu hỏi mẫu* (prompts)? Phần lớn server tích hợp hệ thống mà hecigo gặp rơi vào loại đầu.

## Hai đường truyền, cùng một lõi

Đặc tả định nghĩa hai đường truyền chuẩn, và Ads Insights chạy cả hai từ một mã nguồn.

**stdio, cho một người một máy.** Host khởi động server như một tiến trình con, gửi JSON-RPC vào `stdin`, đọc câu trả lời ở `stdout`, mỗi thông điệp một dòng. Luật quan trọng nhất: server không được ghi bất cứ thứ gì không phải thông điệp MCP ra `stdout`. Log đi `stderr`. Một dòng `console.log` để gỡ lỗi là đủ làm host đọc hỏng luồng. Điểm vào stdio của Ads Insights gần như chỉ có thế:

```ts
const config = configFrom(process.env);
if (!Object.keys(config).filter((k) => k !== "timeoutMs").length) {
  // stderr, không bao giờ stdout: stdout là kênh giao thức.
  process.stderr.write("hecigo-ads-mcp: no platform configured. Starting anyway so ads_health can explain.\n");
}
const server = createServerFromConfig(config);
await server.connect(new StdioServerTransport());
```

Đoạn trên rút gọn từ `src/mcp/index.ts`: bỏ phần bắt lỗi khi khởi động. Chú ý server vẫn chạy khi chưa có khoá nào, để công cụ `ads_health` giải thích được thiếu gì, thay vì tiến trình chết và host chỉ báo "server disconnected".

**Streamable HTTP, cho nhiều người.** Server có một địa chỉ duy nhất, ví dụ `https://ads.hecigo.com/mcp`. Mỗi thông điệp từ client là một request POST mới; server trả về hoặc một JSON, hoặc một luồng SSE. Phiên (`Mcp-Session-Id`) là tuỳ chọn. Bản chạy trên Cloudflare Worker chọn không giữ phiên và trả JSON:

```ts
const transport = new WebStandardStreamableHTTPServerTransport({
  sessionIdGenerator: undefined, // không phiên: không cần Durable Object cho mỗi cuộc hội thoại
  enableJsonResponse: true,      // không công cụ nào gửi tiến độ, nên câu trả lời luôn là một thông điệp
});
await server.connect(transport);
return await transport.handleRequest(request);
```

Mỗi request dựng một server mới và bỏ đi khi xong. Không có gì để mất giữa hai request, nên Worker khởi động lạnh hay chuyển vùng đều không làm đứt cuộc hội thoại.

Lõi gọi API quảng cáo nằm ở `src/core` và không import MCP, Node hay Cloudflare. Hai adapter mỏng phủ lên nó. Đó là lý do một thay đổi ở lõi đến được cả hai đường cùng lúc.

> Related: [Tên chiến dịch ghi 22, target ROAS thật là 0,23: hỏi số liệu quảng cáo ngay trong Claude](https://hecigo.com/blog/ten-chien-dich-ghi-22-hoi-so-lieu-quang-cao-ngay-trong-claude/): Nối Google Ads, Meta Ads, TikTok Ads vào Claude hoặc ChatGPT để hỏi ROAS, target giá thầu, search terms và Performance Max. Chỉ đọc, dùng thử được...

## Thứ trợ lý thật sự đọc: tools/list

Sau bước bắt tay, client gửi `tools/list`. Server trả về mỗi công cụ một bản ghi: `name`, `title`, `description`, `inputSchema` (JSON Schema của tham số), và `annotations`. Đây là toàn bộ thứ trợ lý biết về server của bạn. Công cụ nhỏ nhất của Ads Insights trông thế này, lấy từ phản hồi thật, bỏ hai trường phụ (`$schema` và `execution`):

```json
{
  "name": "ads_health",
  "title": "Check ad platform connections",
  "description": "Which ad platforms are configured for this workspace, and whether each credential still works. Run this before blaming missing data on an empty account.",
  "inputSchema": { "type": "object", "properties": {} },
  "annotations": { "readOnlyHint": true, "openWorldHint": true }
}
```

Câu cuối của `description` không mô tả công cụ. Nó dặn trợ lý khi nào gọi. Đó là cách viết mô tả hecigo rút ra sau ba tuần: viết như dặn một đồng nghiệp mới, không viết như tài liệu API.

Đo trên mã nguồn hiện tại, chạy cục bộ qua stdio, không cấu hình khoá nào, ngày 05/10/2026:

| Phần | Kích thước |
|---|---|
| `instructions` của server (đọc một lần lúc bắt tay) | 702 ký tự |
| Tổng `description` của 9 công cụ | 4.991 ký tự |
| Tổng `inputSchema` của 9 công cụ | 11.998 ký tự |
| Riêng `ads_insights`: mô tả + schema | 1.470 + 5.158 ký tự |
| Toàn bộ phản hồi `tools/list` | 18.778 byte |

Bản dựng còn nằm trong thư mục `dist` từ tối 12/09, lúc đã thêm ba công cụ sau bản đầu, có sáu công cụ và phản hồi `tools/list` dài 5.282 byte. Gấp hơn ba lần trong ba tuần, phần lớn vì `ads_insights` nhận thêm tham số: cột phụ, hành động Meta theo tên, mốc lịch như `last_week`. Mỗi tham số mới là một đoạn mô tả mà trợ lý mang theo dù câu hỏi không cần tới nó. hecigo chưa đo chi phí token của phần này theo từng trợ lý; con số ở trên là byte và ký tự, không phải token.

**Ẩn công cụ thay vì từ chối.** Một workspace chỉ đọc thì công cụ ghi không bao giờ được đăng ký, nên nó không xuất hiện trong `tools/list`. Lý do ghi ngay trong mã: một công cụ trợ lý nhìn thấy là công cụ nó sẽ suy luận và lên kế hoạch quanh, và lời từ chối chỉ tới sau khi trợ lý đã lỡ nói với người dùng rằng nó sửa được chiến dịch.

`readOnlyHint: true` thì vẫn khai, nhưng đừng coi nó là hàng rào. Đặc tả nói client phải coi annotation là không đáng tin trừ khi đến từ server đáng tin. Hàng rào thật nằm ở hai chỗ khác: quyền xin trên nền tảng (với Meta là `ads_read`, quyền đọc báo cáo), và mã nguồn không có đường nào gọi API ghi. Google Ads không có phạm vi quyền chỉ đọc, nên với Google, hàng rào còn lại là mã nguồn và quyền của chính tài khoản Google dùng để uỷ quyền.

## Kết quả trả về: chữ và dữ liệu có cấu trúc

Một lần `tools/call` trả về mảng `content` (chữ, ảnh, link tài nguyên) và có thể kèm `structuredContent`, một đối tượng JSON. Đặc tả khuyên server trả dữ liệu có cấu trúc thì cũng trả bản chữ, để client cũ vẫn đọc được.

Ads Insights trả cả hai trong mọi câu trả lời: bảng chữ cho người và trợ lý đọc, `rows` và `errors` cho trợ lý xử lý tiếp. Không chỉ vì đặc tả khuyên. Ghi chú trong mã nguồn, rút từ lần làm gợi ý nối nền tảng, ghi rằng có trợ lý chỉ đọc phần có cấu trúc và Claude là một trong số đó. Một thông báo chỉ đặt ở phần chữ thì với những trợ lý đó coi như không tồn tại.

Lỗi có hai tầng, và chọn sai tầng là trợ lý hiểu sai:

- **Lỗi giao thức** (JSON-RPC error): tên công cụ không tồn tại, tham số sai kiểu. Trợ lý hiểu là nó gọi sai.
- **Lỗi thực thi** (`isError: true` trong kết quả): API quảng cáo từ chối, nền tảng chưa nối. Trợ lý đọc được nội dung và kể lại cho người dùng.

Khi người dùng hỏi về một nền tảng chưa nối, Ads Insights trả `isError: true` kèm địa chỉ để nối, thay vì trả bảng rỗng. Bảng rỗng là câu trả lời đúng về mặt kỹ thuật và vô dụng với người hỏi: trợ lý sẽ nói "không có dữ liệu" trong khi sự thật là chưa có kết nối.

Một nền tảng hỏng cũng không được làm mất số của hai nền tảng kia. Ba lời gọi chạy song song, lỗi quay về theo từng nền tảng trong `errors`.

## Đăng nhập: một cái 401 và ba địa chỉ tự tìm

Server stdio đọc khoá từ biến môi trường. Server từ xa phải biết ai đang gọi, và người dùng chỉ dán đúng một địa chỉ. Hỏi `https://ads.hecigo.com/mcp` mà không mang token, ngày 05/10/2026:

```text
HTTP/2 401
www-authenticate: Bearer realm="ads-insights", resource_metadata="https://ads.hecigo.com/.well-known/oauth-protected-resource"
```

Từ dòng `resource_metadata` đó, client tự đi tiếp: tài liệu tài nguyên chỉ ra máy chủ cấp quyền là chính `ads.hecigo.com`; tài liệu của máy chủ cấp quyền khai ba địa chỉ (`/oauth/authorize`, `/oauth/token`, `/oauth/register`), chỉ nhận PKCE `S256`, và phương thức xác thực ở token endpoint là `none` vì client công khai không giữ được bí mật. Client tự đăng ký, mở trang đăng nhập Google cho người dùng, rồi đổi mã lấy token. Người dùng chỉ chạm vào hai việc: dán địa chỉ, và bấm cho phép.

Ba luật hecigo giữ ở chặng này: PKCE `S256` bắt buộc; mã uỷ quyền bị xoá trước khi kiểm bất cứ điều gì, nên một lần đổi thất bại cũng tiêu luôn mã; refresh token xoay vòng, token bị trộm dùng được đúng một lần rồi làm hỏng client thật, tức là lộ ra thay vì âm thầm. Token chỉ lưu dạng SHA-256.

## Ba chỗ đã làm sai

**Trợ lý giữ danh sách công cụ cũ.** Khi `ads_insights` có thêm tham số danh sách `metrics`, các cuộc hội thoại đã mở từ trước vẫn giữ schema cũ, không có tham số đó. Trợ lý vẫn thử dùng, nhưng gửi chuỗi: `"reach,cpm"` hoặc `'["reach","cpm"]'`, và schema mới từ chối. Cách sửa là nhận cả chuỗi, tách thành danh sách, rồi mới kiểm từng phần tử với danh sách tên cho phép. Bài học rộng hơn: đổi `tools/list` không đổi ngay thứ trợ lý đang cầm, nên tham số mới phải chịu được cách gọi của trợ lý chưa biết nó.

**Kéo theo cả một khung mà không dùng.** Tới 16/09/2026, đường `/mcp` trên Worker đi qua một gói bọc ngoài SDK. Gói đó kéo theo một MCP client, một bản cài server thứ hai, phần gửi email và cron mà Worker không bao giờ chạy. Bỏ nó, dùng thẳng transport của SDK, bundle giảm từ 3,8 MB xuống 2,0 MB (bản nén từ 748 KB xuống 389 KB). Mỗi isolate khởi động lạnh đều phải đánh giá phần mã thừa đó trước khi trả lời được lời gọi công cụ đầu tiên.

**Phiên bản giao thức đã đi trước.** Server này bắt tay bằng `initialize` và trả `protocolVersion` là `2025-06-18`. Bản đặc tả `2026-07-28` mô tả giao thức không trạng thái: mỗi request tự mang phiên bản và năng lực trong `_meta`, cùng một request `server/discover` để hỏi server hỗ trợ gì. Ads Insights chưa chuyển sang cách đó. Nó vẫn chạy với các client hecigo đã thử, vì client và server thương lượng phiên bản, nhưng đây là việc còn nợ chứ không phải thiết kế đã chọn.

Còn một câu hecigo chưa có đáp án: khi mỗi tham số mới làm dài thêm thứ trợ lý phải đọc trong mọi cuộc hội thoại, ranh giới nằm ở đâu giữa một công cụ nhiều tham số và nhiều công cụ nhỏ, đo bằng số lần trợ lý gọi đúng ngay lần đầu?

> **Muốn xem một MCP server chạy thật?** Thêm địa chỉ `https://ads.hecigo.com/mcp` vào Claude, ChatGPT, Cursor hoặc VS Code, theo [trang hướng dẫn](https://ads.hecigo.com/guide). Đang dựng MCP server cho hệ thống của mình và kẹt ở đăng nhập hay ở cách viết mô tả công cụ thì [nhắn hecigo](https://hecigo.com/#contact).

> Related: [Kiểm tra độ thân thiện của website với AI Agent: Cơ chế hoạt động của Vercel Is Agentic](https://hecigo.com/blog/kiem-tra-do-than-thien-cua-website-voi-ai-agent-co-che-hoat-dong-cua-vercel-is-a/): Is Agentic chấm hecigo.com 73/100. Bốn vòng vá đưa nó lên 100. Lỗi đắt nhất là một phép kiểm mà chính bài này từng lấy làm ví dụ về việc làm đúng.

## Nguồn tham khảo

- [Architecture overview](https://modelcontextprotocol.io/docs/learn/architecture) - Model Context Protocol
- [Transports, bản 2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) - Model Context Protocol
- [Tools, bản 2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) - Model Context Protocol

---

Published by hecigo, middleware & integration lab. https://hecigo.com · hi@hecigo.com
