Xây dựng MCP server
Tiêu chuẩn, từ khoá và cách xử lý xác thực để một MCP server do bạn viết nối được vào Harnix như một connector.
Trang này dành cho đội phát triển muốn tự viết một MCP server (API nội bộ, hệ thống đơn hàng, CRM…) để agent trên Harnix gọi được qua Connector. Harnix là một MCP client từ xa: nó gọi server của bạn qua HTTPS, thay mặt tổ chức đã thêm connector.
Tóm tắt những gì server cần có:
| Hạng mục | Tiêu chuẩn |
|---|---|
| Giao thức | Model Context Protocol, dùng SDK chính thức (Python, TypeScript…) |
| Transport | Streamable HTTP ở một URL duy nhất (ví dụ https://mcp.example.com/mcp); SSE cũ vẫn được hỗ trợ. Không hỗ trợ stdio |
| Mạng | https, tên miền công khai, không chuyển hướng (redirect) URL MCP |
| Xác thực | Một trong bốn cách: không xác thực, header cố định, OAuth (authorization code + PKCE), OAuth client credentials |
| Tool | Tên snake_case, description rõ ràng, inputSchema là JSON Schema, khai readOnlyHint: true cho tool chỉ đọc |
| Kết quả | Trả nội dung dạng text, tối đa 64 KB, dùng isError: true cho lỗi nghiệp vụ |
Yêu cầu kết nối
- URL. Một endpoint MCP, ví dụ
https://mcp.example.com/mcp. URL phải làhttps, trỏ tới địa chỉ công khai; địa chỉ private/nội bộ (10.x,192.168.x,localhost…) và URL chứa username/password bị từ chối. Harnix không đi theo redirect: trả3xxcho URL MCP sẽ thành lỗi. - Transport. Ở chế độ Tự động, Harnix thử Streamable HTTP trước rồi mới tới SSE. Server mới nên dùng Streamable HTTP.
- Thời gian. Bắt tay (
initialize) và liệt kê tool (tools/list) phải xong trong 10 giây. Một lần gọi tool được phép chạy tối đa 120 giây. - Phiên bản giao thức. Harnix khởi tạo với
protocolVersion2025-06-18; SDK chính thức tự thương lượng phiên bản, bạn không cần làm gì thêm. - Danh sách tool được đọc lại mỗi khi một run bắt đầu. Tool mới bạn thêm vào server được bật sẵn cho mọi agent đang dùng connector (admin có thể tắt từng tool trên console).
Chọn cách xác thực
| Cách (trên console) | Chọn khi | Server phải làm |
|---|---|---|
| Không xác thực | Dữ liệu công khai, hoặc server chỉ cho dải IP tin cậy | Không gì |
| Header | API nội bộ, một khoá dùng chung cho cả tổ chức. Cách nhanh nhất cho team nội bộ | Kiểm tra một header cố định |
| Client credentials | Server-to-server qua Identity Provider có sẵn (Keycloak, Auth0, Entra ID…), không cần người đăng nhập | Là OAuth resource server: discovery + kiểm tra access token |
| Tự động → OAuth | Tool phải chạy bằng quyền của một tài khoản người dùng (Google Drive, CRM theo user…) | Là OAuth resource server đầy đủ theo MCP Authorization |
Server dùng Header thì chọn Header, đừng để Tự động
Ở chế độ Tự động, Harnix gửi initialize không kèm gì; nếu server trả
401, Harnix hiểu là server đòi OAuth và đi tìm tài liệu discovery. Một
server chỉ kiểm tra API key sẽ không có tài liệu đó, và connector báo
“Không đọc được cấu hình kết nối của máy chủ này” (discovery_failed).
Header cố định
Admin nhập tên và giá trị header trong sheet connector, ví dụ
Authorization: Bearer <khoá> hoặc X-API-Key: <khoá>. Harnix gửi header
đó trên mọi request tới URL MCP. Giá trị được mã hoá khi lưu và không bao
giờ hiện lại.
Phía server:
- Từ chối request thiếu hoặc sai header bằng
401(hoặc403). - So sánh khoá bằng phép so sánh thời gian hằng (
hmac.compare_digest,crypto.timingSafeEqual), không log giá trị header. - Muốn xoay khoá: chấp nhận cả khoá cũ và mới trong một khoảng thời gian, cập nhật header trên console, rồi bỏ khoá cũ.
Mọi cuộc hội thoại dùng connector này đều đi bằng cùng một khoá — nếu cần phân quyền theo phòng ban, tạo nhiều connector với khoá khác nhau.
OAuth: Harnix đăng nhập thay người dùng
Đây là luồng MCP Authorization: OAuth 2.1, authorization code + PKCE. Server MCP của bạn là resource server; việc cấp token do một authorization server đảm nhận (có thể là chính server đó, hoặc Keycloak/Auth0/Entra ID… của bạn).
Các chuẩn Harnix dùng:
| Chuẩn | Dùng để | Bắt buộc? |
|---|---|---|
| RFC 9728 — OAuth Protected Resource Metadata | Server MCP chỉ ra authorization server của nó | Có |
| RFC 8414 — Authorization Server Metadata (hoặc OpenID Connect Discovery) | Harnix đọc endpoint, phương thức hỗ trợ | Có |
RFC 7636 — PKCE, phương thức S256 | Chống đánh cắp code | Có |
RFC 8707 — Resource Indicators (resource) | Token được cấp riêng cho server MCP của bạn | Có (Harnix luôn gửi) |
| RFC 7591 — Dynamic Client Registration | Harnix tự đăng ký client | Không — thiếu thì admin nhập Client ID tay |
| RFC 7009 — Token Revocation | Thu hồi token khi admin bấm Ngắt kết nối | Không |
RFC 9207 — tham số iss khi redirect về | Chống nhầm authorization server | Không — có thì Harnix kiểm tra |
Bước 1 — Trả 401 kèm WWW-Authenticate
Request tới URL MCP không có token (hoặc token sai, hết hạn) trả về:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="orders.read"resource_metadata— URL tài liệu Protected Resource Metadata. Không có, Harnix thửhttps://mcp.example.com/.well-known/oauth-protected-resource/mcp(theo path của URL MCP) rồi tới/.well-known/oauth-protected-resource.scope(không bắt buộc) — scope cần xin.
Bước 2 — Protected Resource Metadata
{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["orders.read", "orders.write"]
}resourcelà chính URL MCP mà admin nhập vào Harnix.- Harnix dùng phần tử đầu tiên của
authorization_servers.
Bước 3 — Authorization Server Metadata
Harnix đọc https://auth.example.com/.well-known/oauth-authorization-server
(có path thì thêm path phía sau), rồi tới /.well-known/openid-configuration.
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"registration_endpoint": "https://auth.example.com/register",
"revocation_endpoint": "https://auth.example.com/revoke",
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"],
"grant_types_supported": ["authorization_code", "refresh_token"]
}issuerphải khớp URL authorization server ở bước 2.code_challenge_methods_supportedphải cóS256, nếu không connector báo lỗipkce_unsupported.- Mọi endpoint phải là
httpscông khai — cùng luật với URL MCP. - Harnix cache tài liệu này tối đa 24 giờ.
Bước 4 — Đăng ký client
Có registration_endpoint: Harnix tự đăng ký (RFC 7591) với
{
"client_name": "Harnix",
"redirect_uris": ["https://app.harnix.vn/connectors/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}token_endpoint_auth_method là none (public client) nếu server hỗ trợ,
nếu không là client_secret_basic.
Không có registration_endpoint: tạo client trên authorization server với
redirect URI https://app.harnix.vn/connectors/callback, grant
authorization_code + refresh_token, rồi đưa OAuth Client ID (và
OAuth Client Secret nếu là confidential client) cho admin nhập ở mục
Nâng cao. Thiếu cả hai, connector báo registration_unsupported.
Bước 5 — Đăng nhập và đổi code lấy token
Admin bấm Kết nối; Harnix mở authorization_endpoint với:
| Tham số | Giá trị |
|---|---|
response_type | code |
client_id | client đã đăng ký |
redirect_uri | https://app.harnix.vn/connectors/callback |
state | chuỗi ngẫu nhiên, trả nguyên về |
code_challenge, code_challenge_method | PKCE, S256 |
resource | URL MCP |
scope | scope admin cấu hình; không có thì lấy scope của bước 1; không có nữa thì mọi scopes_supported |
Sau khi người dùng đồng ý, redirect về redirect_uri kèm code và state
(thêm iss nếu bạn hỗ trợ RFC 9207). Lượt đăng nhập có hiệu lực 10 phút.
Harnix gọi token_endpoint (form application/x-www-form-urlencoded):
POST /token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret) ← chỉ khi có secret
grant_type=authorization_code&code=…&redirect_uri=https://app.harnix.vn/connectors/callback
&code_verifier=…&resource=https://mcp.example.com/mcp&client_id=…Client có secret: Harnix gửi client_secret_basic nếu metadata liệt kê nó
(hoặc không nói gì), không thì client_secret_post. Public client chỉ gửi
client_id.
Response token:
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "…",
"scope": "orders.read"
}Nên trả expires_in và refresh_token. Không có refresh_token, khi token
hết hạn admin phải bấm Kết nối lại.
Bước 6 — Kiểm tra token trên mỗi request MCP
Harnix gửi Authorization: Bearer <access_token> trên mọi request. Server MCP:
- Xác thực token (chữ ký JWT với JWKS, hoặc token introspection).
- Kiểm tra hạn dùng.
- Kiểm tra audience: token phải được cấp cho chính URL MCP của bạn
(giá trị
resource). Không chấp nhận token cấp cho dịch vụ khác. - Kiểm tra scope cho từng tool.
| Tình huống | Trả về | Harnix làm gì |
|---|---|---|
| Token thiếu, sai, hết hạn | 401 + WWW-Authenticate như bước 1 | Refresh một lần rồi gửi lại; vẫn 401 → Cần kết nối lại |
| Token đúng nhưng thiếu quyền | 403 + WWW-Authenticate: Bearer error="insufficient_scope", scope="orders.write" | Cần kết nối lại; lần kết nối sau xin thêm scope đó |
Refresh và thu hồi
- Harnix refresh trước khi token hết hạn 60 giây (
grant_type=refresh_token, kèmresource), hoặc ngay khi nhận401. - Xoay vòng refresh token (mỗi lần refresh trả refresh token mới) được hỗ trợ; Harnix không bao giờ dùng song song một refresh token.
- Authorization server trả
4xxkhi refresh (ví dụinvalid_grant) → connector chuyển Cần kết nối lại.5xxhay429được coi là lỗi tạm thời, Harnix thử lại ở run sau. - Admin bấm Ngắt kết nối: Harnix gọi
revocation_endpoint(nếu có) cho refresh token rồi access token.
Client credentials
Dùng khi không cần người đăng nhập: Harnix tự lấy token bằng Client ID và
Client secret admin nhập trên console. Server vẫn phải làm bước 1–3 ở
trên (trả 401 + Protected Resource Metadata + Authorization Server Metadata,
metadata vẫn phải liệt kê S256) và bước 6 (kiểm tra token). Harnix gọi:
POST /token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=client_credentials&resource=https://mcp.example.com/mcp&scope=orders.read&client_id=…Không có refresh token: khi token hết hạn hoặc bị 401, Harnix xin token
mới. Authorization server trả 4xx → connector báo Lỗi
(client_credentials_rejected) — admin kiểm tra lại Client ID/secret.
Thiết kế tool
Model đọc tên, mô tả và schema của tool để quyết định gọi tool nào, với tham số gì. Tool viết tốt quan trọng ngang code chạy đúng.
-
Tên:
snake_case, ASCII, động từ + danh từ (get_order_status,create_ticket). Model thấy tên dạngmcp_<tên connector>_<tên tool>, cắt ở 64 ký tự — giữ tên tool ngắn. -
description: tool làm gì, khi nào nên dùng, giới hạn của nó. Viết cho model đọc, không phải cho người. -
inputSchema: JSON Schema đầy đủ — kiểu,required,enum, mô tả từng trường. Kiểm tra lại tham số phía server; đừng tin model. -
Annotation — quyết định có hỏi người dùng trước khi chạy hay không (xem Xác nhận trước khi gọi tool):
Annotation Harnix readOnlyHint: trueChạy ngay Không khai, hoặc readOnlyHint: falseHỏi người dùng cuối trước khi chạy Chỉ đánh dấu
readOnlyHint: truekhi tool thật sự không đổi gì. -
title(không bắt buộc): tên dễ đọc, hiện trên console.
Trả kết quả
- Trả nội dung dạng
text. Harnix chỉ đọc các khốitextvà nối chúng lại; ảnh và tài nguyên nhị phân bị bỏ qua. Dữ liệu có cấu trúc thì trả JSON dưới dạng text. - Kết quả quá 64 KB bị cắt kèm ghi chú. Phân trang hoặc tóm tắt thay vì trả cả bảng dữ liệu.
- Lỗi nghiệp vụ (“không tìm thấy đơn A-1003”) → trả kết quả với
isError: truevà một câu giải thích; model đọc được và trả lời người dùng. Đừng ném exception cho lỗi nghiệp vụ. - Không đưa bí mật, token, stack trace vào kết quả: kết quả vào thẳng prompt và hiện trên trace.
- Idempotency: nếu run bị gián đoạn giữa lúc tool đang chạy, Harnix có thể gọi lại tool đó một lần. Tool ghi dữ liệu nên chống trùng (ví dụ nhận một mã đơn do client sinh, hoặc kiểm tra trùng trước khi tạo).
Ví dụ
# pip install "mcp[cli]"
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations
mcp = FastMCP("orders", stateless_http=True)
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_order_status(order_id: str) -> str:
"""Trạng thái giao hàng của một đơn. Dùng khi khách hỏi đơn đang ở đâu."""
return f"Đơn {order_id} đã giao cho DHL ngày 2026-10-10."
@mcp.tool() # không khai readOnlyHint → Harnix hỏi người dùng trước
def create_order(sku: str, quantity: int) -> str:
"""Tạo đơn hàng mới cho một SKU."""
return f"Đã tạo đơn: {quantity} x {sku}."
if __name__ == "__main__":
mcp.run(transport="streamable-http") # phục vụ ở /mcpPhần OAuth (bước 1–6) thường do một authorization server có sẵn đảm nhận; SDK chính thức có sẵn phần kiểm tra token và Protected Resource Metadata cho resource server — xem tài liệu của SDK bạn dùng.
Kiểm tra trước khi giao
-
Chạy MCP Inspector (
npx @modelcontextprotocol/inspector) với URL của bạn: kết nối được, thấy đủ tool, gọi thử từng tool. -
Với OAuth, kiểm tra discovery bằng
curl:curl -i -X POST https://mcp.example.com/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}' # → 401 và WWW-Authenticate có resource_metadata curl https://mcp.example.com/.well-known/oauth-protected-resource/mcp curl https://auth.example.com/.well-known/oauth-authorization-server # → code_challenge_methods_supported có "S256" -
Thêm connector trên console, chọn đúng cách Xác thực, bấm Kiểm tra: phải thấy Tìm thấy N công cụ.
-
Gán cho một agent thử, hỏi một câu cần tool, mở Lượt chạy & Trace xem tham số và kết quả.
Mã lỗi và cách sửa phía server
| Mã (console hiển thị câu tương ứng) | Nguyên nhân thường gặp | Sửa |
|---|---|---|
discovery_failed | Không có Protected Resource Metadata hoặc Authorization Server Metadata; issuer không khớp; server dùng header nhưng connector để Tự động | Làm đủ bước 1–3, hoặc chọn Header |
pkce_unsupported | Metadata không liệt kê S256 | Bật PKCE S256 trên authorization server |
registration_unsupported | Không có registration_endpoint và chưa nhập Client ID | Bật Dynamic Client Registration, hoặc tạo client tay và nhập ở Nâng cao |
token_exchange_failed | Token endpoint từ chối code — sai redirect_uri, code_verifier, client auth | Kiểm tra redirect URI https://app.harnix.vn/connectors/callback và phương thức client auth |
refresh_rejected | Refresh token bị thu hồi/hết hạn, hoặc server vẫn trả 401 sau khi refresh | Kiểm tra audience/hạn token; admin bấm Kết nối lại |
insufficient_scope | Server trả 403 insufficient_scope | Đúng ý đồ: admin Kết nối lại để cấp thêm scope |
client_credentials_rejected | Client ID/secret sai, hoặc token mới cấp vẫn bị 401 | Kiểm tra client và audience của token |
connection timed out | Không trả lời trong 10 giây (kết nối) / 120 giây (gọi tool) | Tăng tốc initialize/tools/list, kiểm tra firewall |
Tên lớp lỗi (ví dụ ConnectError, ProbeStatusError) | Không kết nối được, hoặc trả mã HTTP không mong đợi (3xx, 5xx) | Kiểm tra URL, chứng chỉ TLS, redirect |
Harnix không bao giờ hiện nguyên văn thông báo lỗi của server (nó có thể chứa bí mật) — chỉ mã ở trên hoặc tên lớp lỗi. Muốn biết chi tiết, xem log phía server của bạn.