Chạy MCP server trên VPS cho AI coding agent
Tự chạy MCP server trên VPS cho AI agent với stdio, HTTP remote, systemd, TLS và auth. Tránh lỗi JSON-RPC bẩn và endpoint tool không xác thực bị lộ.
Bạn sẽ xây dựng gì
Hai MCP setup hoạt động trên cùng một VPS. Đầu tiên là server stdio, chẳng hạn tool filesystem hoặc database mà Claude Code khởi chạy dưới dạng tiến trình con và giao tiếp qua pipe. Sau đó là server HTTP remote chạy lâu dài dưới dạng network service, nằm sau systemd và reverse proxy nginx có TLS. Server này có thể được bất kỳ MCP client nào bạn cấu hình để kết nối truy cập. Cài đặt cho mỗi loại đều ngắn gọn. Phần lớn hướng dẫn này tập trung vào hai vấn đề thường gây lỗi: giữ cho stream JSON-RPC luôn sạch và tuyệt đối không đưa một tool endpoint chưa có xác thực lên Internet công cộng.
MCP thực sự là gì
Model Context Protocol là một cách chuẩn để AI client, Claude Code, Claude Desktop, Gemini CLI trên một VPS hoặc script của bạn gọi các công cụ bên ngoài và đọc các resource bên ngoài. Bản thân model không chạy gì cả. Nó gửi yêu cầu cho client. Client dùng JSON-RPC 2.0 để giao tiếp với một server MCP. Server chạy tool rồi trả kết quả về. Client đó chính là thành phần mà mọi người gọi là agent harness: vòng lặp bao quanh model, quản lý danh sách tool, kiểm tra quyền và trạng thái session. MCP chỉ là cách mở rộng phần tool của vòng lặp này. Chỉ cần viết server một lần, bạn có thể dùng nó với mọi client hỗ trợ MCP. Nếu cách phân tách này còn mới với bạn, đặc biệt là câu hỏi model quyết định gọi tool như thế nào, hãy xem lộ trình từng bước về nền tảng agent trong khoảng một giờ trước khi cấp credential thật cho các server này.
Có 2 transport. Phần còn lại của hướng dẫn này được chia theo 2 transport đó:
- stdio. Client khởi chạy server dưới dạng child process rồi trao đổi các message JSON-RPC được phân tách bằng newline qua standard input và standard output. Không có network, không có port, không có auth; ranh giới tin cậy chính là process. Hầu hết tool chạy local đều dùng cách này.
- Streamable HTTP (và biến thể cũ hơn là HTTP+SSE). Server là một web service chạy liên tục. Client kết nối qua HTTP, còn server có thể stream response về dưới dạng Server-Sent Events. Cách này phù hợp khi bạn muốn chia sẻ một server cho nhiều client hoặc chạy một tool phải luôn tồn tại trên máy.
Chọn stdio khi tool chỉ phục vụ một máy và một user. Chọn HTTP khi tool là một service dùng chung.
Điều kiện cần và những điểm cần lưu ý
Giả sử bạn dùng một Ubuntu 24.04 KVM VPS mới cài, có quyền root hoặc sudo. Ngoài ra, bạn cần:
- Runtime mà server sử dụng. Phần lớn server tham chiếu dùng Node hoặc Python. Ubuntu 24.04 cung cấp Node 18, trong khi một số MCP package hiện tại yêu cầu Node 20 trở lên. Vì vậy, hãy cài bản LTS hiện tại từ NodeSource hoặc nvm thay vì mặc định dùng
apt. Python 3.12 đã có sẵn. - Một domain và DNS A record, nhưng chỉ cần cho remote HTTP server. TLS cần một tên miền phân giải về VPS này. Ví dụ stdio hoàn toàn không cần DNS.
- 512 MB RAM là đủ. MCP server là các tiến trình JSON-RPC gọn nhẹ. Lượng RAM sử dụng phụ thuộc vào tool mà bạn gọi, chẳng hạn database driver hoặc file cache, chứ không phụ thuộc vào protocol.
- Spec này còn mới và vẫn đang thay đổi. Bản revision 2025-03-26 đã thay HTTP+SSE bằng Streamable HTTP và đánh dấu SSE là deprecated. SSE vẫn hoạt động và nhiều server vẫn sử dụng nó. Vì vậy, hãy xem mọi thiết lập transport cố định là thứ cần kiểm tra lại trong release notes của server, thay vì mặc định coi đó là thông tin chắc chắn.
Bước 1: kết nối stdio server với Claude Code
Bắt đầu với filesystem server. Đây là server chính thức, được duy trì tích cực và chỉ cần Node. Lệnh duy nhất dưới đây đăng ký server với Claude Code và giới hạn phạm vi ở project hiện tại, để cấu hình được ghi vào một file có thể commit:
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/apiDấu phân cách -- rất quan trọng: mọi thứ sau đó là command mà Claude Code sẽ chạy, không phải flag của Claude Code. Lệnh này tạo một .mcp.json tại thư mục gốc của project:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}Chưa có gì đang chạy. Khi bạn khởi động Claude Code trong thư mục này lần tiếp theo, agent sẽ đọc .mcp.json, tạo npx -y @modelcontextprotocol/server-filesystem ... dưới dạng child process và thực hiện MCP handshake qua stdin/stdout của process đó. Xác nhận cấu hình đã hoạt động:
claude mcp listServer hoạt động bình thường sẽ in command của nó và dấu tick màu xanh, filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Trong session, slash command /mcp liệt kê các tool mà server cung cấp (read_file, write_file, list_directory), và agent có thể gọi chúng trên các path bạn đã cho phép. Database tool cũng có cấu trúc tương tự: thay package và truyền connection string làm argument cuối cùng. Tuy nhiên, hãy kiểm tra repository của chính server để biết package name hiện tại, vì reference Postgres server đã đổi maintainer nhiều lần.
Đây chính là mục đích của việc chạy agent trên máy chủ: session Claude Code chạy trên VPS bên trong tmux, còn các stdio server chạy ngay bên cạnh nó với quyền truy cập trực tiếp vào file project và các service cục bộ, không phải đi qua network. Khi agent có cả write_file và read_file, bạn nên kết hợp quyền truy cập đó với một skill hướng agent đến thay đổi nhỏ nhất nhưng vẫn hoạt động, vì filesystem tool khiến một lần rewrite lan rộng cũng rẻ như một bản sửa hai dòng. Cách kết nối này cũng mở rộng ra ngoài các file cục bộ: nếu bạn đã chạy search engine trên VPS, bạn có thể cấp cho agent instance SearXNG của chính bạn làm search tool, nhờ đó các truy vấn vẫn nằm trên máy của bạn nhưng nội dung trang không đáng tin cậy được đưa thẳng vào context mà agent sẽ tiếp tục xử lý.
Bước 2: xây dựng HTTP server từ xa
Một server stdio sẽ dừng cùng tiến trình cha và được khởi chạy một lần cho mỗi client. Vì vậy, nếu bạn chạy hai phiên Claude Code trên máy chủ và cho chúng chuyển công việc cho nhau, mỗi phiên sẽ có một bản sao riêng của tool. Khi cần một tool chạy liên tục cho mọi client, chẳng hạn một tool vận hành dùng chung, một database gateway, hoặc một tool được cả laptop và CI gọi đến, bạn cần HTTP transport và một service thực sự. Đây là server Python tối thiểu sử dụng SDK chính thức và cung cấp một tool:
# /opt/mcp-ops/server.py
from mcp.server.fastmcp import FastMCP
import subprocess
mcp = FastMCP("ops-tools", host="127.0.0.1", port=8000)
@mcp.tool()
def disk_free() -> str:
"""Return `df -h` for the server."""
out = subprocess.run(["df", "-h"], capture_output=True, text=True)
return out.stdout
if __name__ == "__main__":
# Serves Streamable HTTP at /mcp on 127.0.0.1:8000
mcp.run(transport="streamable-http")Lưu ý host="127.0.0.1". Server chỉ bind vào localhost, nên không gì bên ngoài máy chủ có thể truy cập trực tiếp. Đây chính xác là điều bạn muốn trước khi có auth. Cài đặt server trong virtualenv riêng để systemd có đường dẫn interpreter ổn định:
sudo useradd --system --home /opt/mcp-ops --shell /usr/sbin/nologin mcp
sudo install -d -o mcp -g mcp /opt/mcp-ops
sudo -H -u mcp python3 -m venv /opt/mcp-ops/.venv
sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install "mcp[cli]"Bước 3: duy trì hoạt động bằng systemd
Một công cụ không hoạt động khi agent cần dùng còn tệ hơn không có công cụ. Điều này đặc biệt quan trọng khi client cũng là một tiến trình chạy lâu dài: một agent luôn hoạt động, giữ nguyên bộ nhớ và lịch chạy qua các lần reboot sẽ gọi các công cụ này theo lịch mà không có ai theo dõi, vì vậy server cũng phải tự khởi động lại.
Tạo /etc/systemd/system/mcp-ops.service:
[Unit]
Description=MCP ops-tools server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/mcp-ops
ExecStart=/opt/mcp-ops/.venv/bin/python /opt/mcp-ops/server.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.targetĐường dẫn tuyệt đối đến Python trong venv tại ExecStart là bắt buộc. Trỏ nó đến /usr/bin/python3 để tiến trình khởi động bằng ModuleNotFoundError: No module named 'mcp', vì system interpreter chưa biết pip install của bạn. Bật và kiểm tra:
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-ops
sudo systemctl status mcp-ops
curl -si -H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-X POST http://127.0.0.1:8000/mcpstatus phải hiển thị active (running). curl trả về HTTP/1.1 400 Bad Request kèm lỗi JSON-RPC trong phần body. Request không có session và cũng không có payload JSON hợp lệ. Đây chính xác là kết quả cần có: nó chứng minh cổng đang trả lời và giao tiếp đúng protocol. Connection refused hoặc phản hồi rỗng nghĩa là tiến trình không bind tại vị trí bạn nghĩ. Hãy xem journalctl -u mcp-ops -n 50.
Bước 4: đặt TLS và reverse proxy phía trước
Server lắng nghe trên localhost. Để truy cập từ bất kỳ đâu, bạn termination TLS tại nginx rồi proxy vào bên trong. Cài nginx, lấy certificate bằng Certbot và Let’s Encrypt trên nginx, sau đó viết location block. Phần quan trọng là tắt buffering, vì hành vi mặc định của nginx giữ response cho đến khi hoàn tất. Điều này làm SSE stream bị treo vĩnh viễn:
server {
listen 443 ssl;
server_name mcp.example.com;
# ssl_certificate lines managed by Certbot
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
# The four lines that make SSE work through nginx:
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding off;
}
}Reload bằng sudo nginx -t && sudo systemctl reload nginx. Nếu bạn đã chạy một fleet container, Traefik sẽ tự thực hiện công việc tương tự bằng reverse proxy Traefik với TLS tự động. Nó cấp certificate và định tuyến theo hostname; bạn chỉ cần thêm label vào container MCP. Dù dùng cách nào, reverse proxy hiện là thành phần duy nhất nằm trên public port và đang trỏ đến một service chưa được bảo vệ. Hãy xử lý việc đó trước khi đăng ký URL ở bất kỳ đâu.
Bước 5: quy tắc bảo mật quan trọng nhất trong chủ đề này
Tuyệt đối không public một MCP endpoint không có authentication. MCP server không phải là API chỉ đọc. Nó cấp quyền truy cập tool vào file, database và đôi khi cả shell của bạn. Một /mcp mở trên Internet public cho phép người lạ có phạm vi truy cập giống hệt AI agent của bạn: họ liệt kê các tool rồi gọi chúng. Hãy xử lý endpoint này chính xác như một admin socket không có authentication, vì bản chất của nó là như vậy. Mức độ ảnh hưởng của một token bị đánh cắp cũng phụ thuộc vào server phía sau: MCP server chỉ đọc đi kèm openGym workout tracker chỉ có thể trả về dữ liệu tập luyện, còn tool filesystem hoặc shell có thể trao quyền kiểm soát cả máy chủ.
Ba lớp phòng vệ, theo thứ tự ưu tiên:
- Không publish endpoint. Giữ server trên
127.0.0.1và truy cập từ laptop qua SSH tunnel:ssh -L 8000:127.0.0.1:8000 matt@vps, sau đó trỏ client đếnhttp://127.0.0.1:8000/mcp. Không có gì bị expose. - Đặt endpoint trên private network. Bind địa chỉ tunnel của WireGuard VPN tự host và chỉ cho các VPN peer truy cập. Internet public sẽ chỉ thấy một cổng đã đóng.
- Nếu bắt buộc phải public, hãy yêu cầu token. Cách đúng là dùng MCP OAuth flow mà HTTP transport hỗ trợ native. Mức tối thiểu thực tế là một shared bearer token được kiểm tra tại proxy. Cách này rẻ và chặn hoàn toàn các request dò quét tự phát:
location /mcp {
if ($http_authorization != "Bearer REPLACE_WITH_LONG_RANDOM") {
return 401;
}
proxy_pass http://127.0.0.1:8000;
# ...buffering-off block from above...
}Tạo token bằng openssl rand -hex 32, và không bao giờ bind chính server vào 0.0.0.0 nếu không đặt một trong các lớp bảo vệ này ở phía trước. Sau đó client gửi token trong header. Trong Claude Code:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Đặt MCP_TOKEN trong shell để secret không bao giờ được ghi dạng plaintext vào .mcp.json; Claude Code sẽ mở rộng ${MCP_TOKEN} từ environment khi đọc giá trị.
Tất cả các lớp bảo vệ trên đều bảo vệ endpoint, không bảo vệ agent đã có token. Đây là nửa còn lại của vấn đề: nếu client của bạn là DeepSeek Harness, các plugin giới hạn những tool mà agent được phép gọi và quét output của tool để phát hiện instruction bị chèn sẽ xử lý phần đó.
Bước 6: debug bằng MCP Inspector
Khi server hoạt động không đúng, đừng phỏng đoán từ bên trong agent. Hãy dùng Inspector để điều khiển server trực tiếp. Đây là test client chính thức trên web. Với server stdio, truyền cho nó chính lệnh mà agent chạy:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpInspector khởi động UI tại http://localhost:6274. Các phiên bản gần đây in ra một URL có query string MCP_PROXY_AUTH_TOKEN. Hãy dùng đúng link đó, nếu không UI sẽ từ chối kết nối. Inspector cũng khởi động một proxy tại cổng 6277. Nhấp Connect, sau đó List Tools, rồi Call Tool với các đối số thực tế. Nếu server hoạt động trong Inspector nhưng lỗi trong agent, lỗi nằm ở cấu hình client, không phải server. Với server HTTP từ xa, chọn transport Streamable HTTP, nhập https://mcp.example.com/mcp, thêm header Authorization rồi kết nối. Đây là cách nhanh nhất để xác nhận auth và proxy đã đúng trước khi đưa agent vào.
Cập nhật server
MCP phát triển nhanh, vì vậy hãy cập nhật bản vá theo lịch. Node server được khởi chạy bằng npx -y sẽ tải phiên bản mới nhất mỗi lần spawn. Cách này tiện nhưng không tái lập được môi trường. Khi server đã quan trọng, hãy pin chính xác phiên bản đã kiểm thử. Đọc phiên bản đó từ npm view @modelcontextprotocol/server-filesystem version rồi thêm vào tên package trong .mcp.json (@modelcontextprotocol/server-filesystem@<version>). Sau đó, chỉ nâng phiên bản khi đã chủ động quyết định. Python server chạy dưới systemd được cập nhật bằng sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]", rồi chạy sudo systemctl restart mcp-ops. Khi nâng cấp, hãy kiểm tra revision của spec mà SDK của bạn nhắm tới. Việc chuyển qua ranh giới SSE sang Streamable-HTTP có thể làm thay đổi transport mà client phải yêu cầu.
Các chế độ lỗi và chuỗi bạn sẽ thấy
Agent báo máy chủ khởi động thất bại. claude mcp list in ✗ Failed to connect và TUI báo MCP server 'filesystem' failed to start. Chạy claude --debug. Thông thường bạn sẽ thấy Error: spawn npx ENOENT vì command không có trong PATH của agent. Runtime bị thiếu hoặc không nằm ở vị trí agent tìm kiếm: chưa cài Node, thiếu npx hoặc Python trong virtualenv được tham chiếu bằng tên lệnh ngắn. Sửa command thành đường dẫn tuyệt đối hoặc cài runtime, rồi kết nối lại.
Stdio server kết nối rồi ngắt ngay lập tức. Client ghi nhận lỗi phân tích JSON, chẳng hạn Unexpected token 'S', "Server sta"... is not valid JSON hoặc Failed to parse message. Nguyên nhân luôn giống nhau: server ghi một dòng log vào stdout. Với stdio, stdout chính là kênh JSON-RPC. Vì vậy, mọi văn bản thừa đều làm hỏng stream và khiến quá trình bắt tay thất bại. Trong Node, console.log ghi ra stdout; hãy dùng console.error. Trong Python, print() dạng đơn giản ghi ra stdout. Hãy ghi log bằng logging được cấu hình thành sys.stderr hoặc truyền file=sys.stderr. Quy tắc này là bắt buộc: với stdio, stdout chỉ chứa JSON-RPC; mọi nội dung dành cho người đọc phải ghi ra stderr.
Remote server hết thời gian chờ hoặc đóng kết nối giữa quá trình bắt tay. Client lỗi với MCP error -32000: Connection closed hoặc Inspector bị treo tại Connect và không bao giờ liệt kê tool. Khi chạy sau nginx, nguyên nhân là buffering: proxy giữ lại SSE stream thay vì flush, nên client chờ một response không bao giờ đến. Thêm proxy_buffering off; và phần còn lại của block trong Bước 4 vào location. Xác nhận bằng curl -N với public URL. Bạn phải thấy event data đến dần từng phần, không phải tất cả xuất hiện cùng lúc ở cuối.
Xác thực bị từ chối. Client báo Error POSTing to endpoint (HTTP 401) hoặc ghi rõ 401 Unauthorized. Có thể header bị thiếu, token sai hoặc biến shell rỗng khi client đọc config. Đây là lỗi thường gặp vì ${MCP_TOKEN} sẽ được thay thành chuỗi rỗng nếu biến chưa được set, sau đó nginx thấy Bearer không có giá trị. In giá trị biến, thêm lại header và xác minh từng byte khớp với token trong if của nginx.
Service không khởi động được dưới systemd. journalctl -u mcp-ops hiển thị ModuleNotFoundError: No module named 'mcp'; ExecStart trỏ đến Python hệ thống thay vì interpreter của venv. Hoặc Address already in use xuất hiện vì process khác đang giữ cổng 8000; tìm process đó bằng sudo ss -ltnp | grep 8000.
FAQ
MCP server chính xác là gì?
Đó là một chương trình cung cấp tools và resources cho AI client thông qua Model Context Protocol, sử dụng JSON-RPC 2.0. Model AI không tự chạy tool. Nó yêu cầu client chạy tool, client gọi MCP server, rồi server thực thi và trả về kết quả. Vì protocol này là tiêu chuẩn, một server có thể hoạt động với mọi client tương thích, chẳng hạn Claude Code, Claude Desktop hoặc Gemini CLI.
stdio và HTTP transport khác nhau như thế nào?
stdio server được client khởi chạy dưới dạng child process và giao tiếp qua stdin/stdout. Vì vậy, nó tồn tại và dừng cùng một client trên một máy, đồng thời không cần network hoặc auth. HTTP server là network service chạy lâu dài mà nhiều client có thể truy cập cùng lúc. Vì vậy, nó cần TLS và authentication. Dùng stdio cho các tool cục bộ, chỉ phục vụ một user. Dùng HTTP (Streamable HTTP trên các server hiện tại) cho mọi thứ cần chia sẻ hoặc chạy lâu dài.
Làm cách nào để bảo mật MCP server từ xa?
Hãy giả định server cấp quyền truy cập tool vào file, database hoặc shell của bạn, và tuyệt đối không expose server khi chưa có authentication. Cách tốt nhất là giữ server chỉ bind vào localhost, rồi truy cập qua SSH tunnel hoặc private VPN. Nếu bắt buộc phải public, hãy đặt server phía sau reverse proxy để bắt buộc dùng bearer token hoặc MCP OAuth flow. Tạo token bằng openssl rand -hex 32 và không bao giờ bind server vào 0.0.0.0 nếu không có một trong các lớp bảo vệ này ở phía trước.
Làm cách nào để debug server không khởi động?
Trước tiên, kiểm tra claude mcp list và ✗ Failed to connect bằng spawn ... ENOENT. Điều này có nghĩa là command hoặc runtime bị thiếu, nên hãy sửa path hoặc cài đặt nó. Nếu server kết nối được rồi ngắt với lỗi phân tích JSON, server đang ghi log vào stdout và làm hỏng luồng JSON-RPC. Hãy chuyển toàn bộ log sang stderr. Với các trường hợp khác, hãy chạy đúng command đó trong MCP Inspector. Công cụ này điều khiển server độc lập, giúp bạn phân biệt lỗi của server với lỗi cấu hình client.