Kết nối hệ thống nội bộ với Claude Code qua MCP — từ server fetch chung đến viết MCP Server chuyên dụng, bao gồm nền tảng deploy, giám sát, hệ thống ticket và nguyên tắc thiết kế công cụ.
Bài trước đã nói về kết nối database. Database có giao thức chuẩn nên việc tích hợp khá đơn giản. Nhưng phần lớn các team hàng ngày còn phụ thuộc vào hàng loạt hệ thống nội bộ: nền tảng deploy, dashboard giám sát, hệ thống ticket, API nội bộ, trung tâm cấu hình.
Những hệ thống này thường không có sẵn MCP Server, nhưng hầu như tất cả đều cung cấp HTTP API. Bài viết này hướng dẫn cách dùng MCP để kết nối các công cụ nội bộ này vào Claude Code, giúp Claude trực tiếp kiểm tra monitoring, xem trạng thái deploy, thao tác với ticket.
Có hai cách để tích hợp công cụ nội bộ:
Cách 1: Dùng MCP Server HTTP tổng quát
Cộng đồng có một số MCP Server tổng quát, có thể bọc bất kỳ REST API nào thành công cụ MCP. Bạn chỉ cần viết file mô tả API, server sẽ chuyển đổi thành công cụ mà Claude có thể gọi. Phù hợp với các trường hợp API đơn giản, không cần logic phức tạp.
Cách 2: Tự viết MCP Server
Dùng MCP SDK của TypeScript hoặc Python để viết một Server chuyên dụng, kiểm soát hoàn toàn việc định nghĩa công cụ, validate tham số, xử lý lỗi. Phù hợp khi cần kết hợp nhiều API, chuyển đổi dữ liệu, hoặc thêm logic nghiệp vụ.
Bài viết này sẽ trình bày cả hai cách, bắt đầu từ cách đơn giản.
Giải pháp nhẹ nhất là dùng @anthropic-ai/mcp-server-fetch của Anthropic, cho phép Claude gửi HTTP request trực tiếp. Cấu hình cực kỳ đơn giản:
{
"mcpServers": {
"fetch": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-server-fetch"]
}
}
}
Sau khi cấu hình xong, Claude có thể gọi API nội bộ ngay:
Giúp tôi kiểm tra trạng thái hiện tại của nền tảng deploy https://deploy.internal.com/api/v1/services/user-service
Claude sẽ gửi GET request, nhận response và phân tích kết quả cho bạn.
Nhưng cách này có những hạn chế rõ ràng:
Phù hợp để dùng tạm, không phù hợp làm giải pháp lâu dài.
Khi bạn cần sử dụng một hệ thống nội bộ thường xuyên, viết một MCP Server chuyên dụng là lựa chọn tốt hơn. Dưới đây là demo với tình huống thực tế: tích hợp nền tảng deploy của công ty.
Giả sử nền tảng deploy của bạn cung cấp các API sau:
GET /api/v1/services — Liệt kê tất cả serviceGET /api/v1/services/:name/status — Xem trạng thái servicePOST /api/v1/services/:name/deploy — Kích hoạt deployGET /api/v1/services/:name/logs — Xem log deploy gần đâyKhởi tạo project trước:
mkdir mcp-deploy && cd mcp-deploy
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init
Code chính src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const API_BASE = process.env.DEPLOY_API_URL!;
const API_TOKEN = process.env.DEPLOY_API_TOKEN!;
async function api(path: string, method = "GET", body?: unknown) {
const res = await fetch(`${API_BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${API_TOKEN}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
throw new Error(`API error: ${res.status} ${await res.text()}`);
}
return res.json();
}
const server = new McpServer({
name: "deploy-platform",
version: "1.0.0",
});
// Liệt kê tất cả service
server.tool("list_services", "列出部署平台上的所有服务及其状态", {}, async () => {
const data = await api("/api/v1/services");
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
});
// Xem trạng thái một service cụ thể
server.tool(
"service_status",
"查看指定服务的当前部署状态、版本号和健康检查结果",
{ name: z.string().describe("服务名称,如 user-service") },
async ({ name }) => {
const data = await api(`/api/v1/services/${name}/status`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
// Xem log deploy
server.tool(
"deploy_logs",
"查看指定服务最近的部署日志",
{
name: z.string().describe("服务名称"),
limit: z.number().optional().default(10).describe("返回条数,默认 10"),
},
async ({ name, limit }) => {
const data = await api(`/api/v1/services/${name}/logs?limit=${limit}`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
// Kích hoạt deploy
server.tool(
"trigger_deploy",
"触发指定服务的部署。这是一个写操作,会实际影响生产环境",
{
name: z.string().describe("服务名称"),
version: z.string().describe("要部署的版本号或 git ref"),
},
async ({ name, version }) => {
const data = await api(`/api/v1/services/${name}/deploy`, "POST", {
version,
});
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
);
const transport = new StdioServerTransport();
server.connect(transport);
Biên dịch:
npx tsc
{
"mcpServers": {
"deploy": {
"command": "node",
"args": ["/path/to/mcp-deploy/dist/index.js"],
"env": {
"DEPLOY_API_URL": "https://deploy.internal.com",
"DEPLOY_API_TOKEN": "your-api-token"
}
}
}
}
Đặt token trong .claude/settings.local.json (không commit vào git), URL đặt trong .claude/settings.json (commit vào git, chia sẻ với team).
Sau khi cấu hình xong, cuộc hội thoại trở nên tự nhiên hơn:
user-service hiện đang ở trạng thái gì?
→ Claude gọi service_status("user-service")
→ Kết quả: đang chạy, phiên bản v2.3.1, deploy lần cuối 2 giờ trước, health check đều pass
Mấy lần deploy gần đây có lần nào thất bại không?
→ Claude gọi deploy_logs("user-service", 20)
→ Phân tích log, cho biết lần deploy thứ 3 đã bị rollback do health check timeout
Deploy user-service lên v2.3.2
→ Claude gọi trigger_deploy("user-service", "v2.3.2")
→ Vì mô tả công cụ ghi rõ "thao tác ghi", Claude sẽ xác nhận với bạn trước
Đây là câu hỏi cần cân nhắc nghiêm túc.
Thao tác đọc — cứ tích hợp thoải mái. Kiểm tra trạng thái, xem log, tìm ticket — những thao tác này không có tác dụng phụ, Claude có làm sai cũng không thiệt hại gì.
Thao tác ghi chia làm hai loại:
Thao tác ghi rủi ro thấp có thể tích hợp, nhưng phải ghi rõ trong mô tả công cụ. Claude sẽ tự động yêu cầu người dùng xác nhận với những thao tác được đánh dấu có tác dụng phụ. Ví dụ: tạo ticket, gửi tin nhắn, cập nhật cấu hình.
Thao tác ghi rủi ro cao nên tránh tích hợp. Xóa tài nguyên, kích hoạt rollback, thay đổi quyền — hậu quả của những thao tác này nghiêm trọng và không thể hoàn tác, để thao tác thủ công sẽ an toàn hơn.
Nếu nhất định phải tích hợp thao tác ghi, ít nhất hãy làm hai việc:
| Hệ thống | Công cụ cung cấp | Lưu ý |
|---|---|---|
| Nền tảng Deploy (K8s / Kamal) | Xem trạng thái service, xem log, kích hoạt deploy | Thêm xác nhận cho thao tác ghi |
| Hệ thống Giám sát (Grafana / Datadog) | Xem metric, xem lịch sử cảnh báo | Giới hạn khoảng thời gian truy vấn, tránh kéo quá nhiều dữ liệu |
| Hệ thống Ticket (Jira / Linear) | Tìm ticket, tạo ticket, cập nhật trạng thái | Tạo ticket là thao tác ghi nhưng rủi ro thấp |
| Tài liệu Nội bộ (Notion / Confluence) | Tìm tài liệu, đọc nội dung trang | Chú ý phân trang, không kéo quá nhiều một lần |
| Trung tâm Cấu hình (Consul / etcd) | Đọc cấu hình, so sánh khác biệt giữa các môi trường | Chỉ đọc, không tích hợp thao tác ghi |
| CI/CD (GitHub Actions / Jenkins) | Xem trạng thái build, kích hoạt build | Kích hoạt build thuộc thao tác ghi rủi ro trung bình |
Viết công cụ MCP khác với viết API. API dành cho lập trình viên, công cụ dành cho AI. Có một số nguyên tắc đáng lưu ý:
Tên công cụ phải rõ ràng, dễ hiểu
✗ get_svc_stat — Claude chưa chắc đoán đúng nghĩa của viết tắt
✓ service_status — Nhìn là biết ngay chức năng
Mô tả viết cho AI đọc
Mô tả công cụ không phải tài liệu cho người đọc, mà là căn cứ để Claude quyết định "khi nào nên gọi công cụ này". Mô tả cần nêu rõ: công cụ này làm gì, trả về gì, khi nào nên dùng.
✗ "Lấy trạng thái service"
✓ "Xem trạng thái deploy hiện tại, số phiên bản và kết quả health check của service được chỉ định. Sử dụng khi người dùng hỏi liệu một service có đang chạy bình thường không"
Định nghĩa tham số rõ ràng bằng zod
Tham số có .describe() thì Claude mới biết cần điền gì. Không có mô tả, Claude chỉ có thể đoán dựa vào tên tham số.
Trả về dữ liệu có cấu trúc
Công cụ MCP trả về text, nhưng hãy cố gắng trả về JSON đã format. Claude xử lý dữ liệu có cấu trúc chính xác hơn nhiều so với văn bản thuần.
Độ chi tiết vừa phải
Đừng nhồi một quy trình phức tạp vào một công cụ. Cũng đừng tách một truy vấn đơn giản thành ba công cụ. Nguyên tắc là: một công cụ hoàn thành một thao tác độc lập, có ý nghĩa.
Code MCP Server có thể đặt ở một số vị trí:
Trong repository của project (khuyến nghị khi bắt đầu)
your-project/
├── .claude/settings.json
├── mcp-servers/
│ └── deploy/
│ ├── src/index.ts
│ ├── package.json
│ └── tsconfig.json
└── ...
Ưu điểm là code và cấu hình nằm cùng chỗ, team clone về cài dependency là dùng được ngay.
Repository riêng
Khi MCP Server cần dùng cho nhiều project, đặt ở repository riêng, publish thành npm package hoặc Docker image.
{
"mcpServers": {
"deploy": {
"command": "npx",
"args": ["-y", "@yourcompany/mcp-deploy-server"]
}
}
}
Cài đặt toàn cục
Với những MCP Server dùng chung toàn công ty (ví dụ tích hợp xác thực tập trung, nền tảng log tập trung), cài toàn cục rồi cấu hình trong ~/.claude/settings.json.
Vấn đề thường gặp nhất khi phát triển MCP Server là "Claude không gọi công cụ của tôi" hoặc "gọi rồi nhưng bị lỗi".
Xác nhận Server đã khởi động
Sau khi khởi động lại Claude Code, gõ /mcp để xem danh sách MCP Server đã kết nối. Nếu Server của bạn không có trong danh sách, kiểm tra lại command và args.
Test Server độc lập
MCP Server giao tiếp qua stdio, có thể test trực tiếp trong terminal:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js
Nếu trả về danh sách công cụ, nghĩa là bản thân Server không có vấn đề.
Xem log gọi công cụ của Claude
Claude Code hiển thị input và output của mỗi lần gọi công cụ. Nếu tham số truyền sai, thường là do mô tả công cụ hoặc định nghĩa tham số chưa đủ rõ ràng, khiến Claude hiểu nhầm.
Hãy kết nối tất cả lại bằng một ví dụ thực tế. Giả sử bạn muốn tích hợp Sentry vào Claude Code để có thể trực tiếp kiểm tra lỗi trên production.
server.tool(
"search_errors",
"在 Sentry 中搜索最近的错误。用于排查线上问题、查看错误趋势",
{
query: z.string().describe("搜索关键词,如错误信息、函数名"),
hours: z.number().optional().default(24).describe("查看最近多少小时的错误"),
},
async ({ query, hours }) => {
const since = new Date(Date.now() - hours * 3600000).toISOString();
const data = await api(
`/api/0/projects/${ORG}/${PROJECT}/issues/?query=${encodeURIComponent(query)}&start=${since}&sort=date`
);
const summary = data.map((issue: any) => ({
title: issue.title,
count: issue.count,
firstSeen: issue.firstSeen,
lastSeen: issue.lastSeen,
link: issue.permalink,
}));
return {
content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
};
}
);
server.tool(
"error_details",
"查看 Sentry 中某个错误的详细信息,包括堆栈和最近一次事件",
{ issueId: z.string().describe("Sentry issue ID") },
async ({ issueId }) => {
const [issue, latest] = await Promise.all([
api(`/api/0/issues/${issueId}/`),
api(`/api/0/issues/${issueId}/events/latest/`),
]);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
title: issue.title,
count: issue.count,
users: issue.userCount,
stacktrace: latest.entries?.find(
(e: any) => e.type === "exception"
),
},
null,
2
),
},
],
};
}
);
Sau khi tích hợp xong, cuộc hội thoại khi điều tra sự cố production trở thành:
4 giờ gần đây có lỗi 500 mới nào không?
→ Claude tìm kiếm trên Sentry
→ Phát hiện 3 issue mới, nghiêm trọng nhất ảnh hưởng 120 người dùng
→ Tự động kéo stack trace, xác định là null pointer exception
→ Tìm vị trí tương ứng trong code, đưa ra phương án sửa lỗi
Từ phát hiện vấn đề đến định vị code, toàn bộ quá trình hoàn thành trong một cuộc hội thoại.
Bài viết này đã trình bày cách dùng MCP để tích hợp công cụ nội bộ. Ý tưởng cốt lõi là: hệ thống nội bộ có HTTP API → viết một MCP Server bọc lại → Claude có thể dùng trực tiếp.
Tất cả ví dụ trong bài đều là bọc lại API có sẵn — nền tảng deploy, Sentry vốn đã có interface, MCP Server chỉ làm nhiệm vụ chuyển tiếp và thích ứng. Bài tiếp theo sẽ nói về một tình huống khác: khi khả năng bạn cần hoàn toàn không có API sẵn, làm thế nào để xây dựng MCP Server từ đầu, tự triển khai logic, quản lý state, và xử lý tương tác đa bước phức tạp.