· cursor / mcp / typescript
Cách xây dựng MCP tools tùy chỉnh hoạt động trong Cursor
Hướng dẫn từng bước xây dựng MCP server tùy chỉnh bằng TypeScript và kết nối với Cursor Pro. Đăng ký tool, quirk khi restart, và năm lỗi cấu hình thường gặp.
Bởi Ethan
1.500 từ · 8 phút đọc
Bạn cần Cursor Pro ($20/tháng) để dùng MCP. Tính năng này không có trên gói Hobby miễn phí. Nếu bạn vẫn đang dùng Hobby, bài viết này là bản xem trước những gì bạn sẽ mở khóa khi nâng cấp.
Khi đã lên Pro, MCP biến Cursor Chat thành thứ gì đó gần với một local agent: nó có thể gọi các function bạn định nghĩa, đọc filesystem, chạy test suite, gọi internal API — bất kỳ thứ gì bạn expose ra như một tool. Bài viết này xây hai tool từ đầu, đăng ký chúng trong Cursor, và giải thích năm lỗi cấu hình khiến hầu hết mọi người mất cả buổi chiều.
Bài này dành cho ai
Developer dùng Cursor hàng ngày và muốn mở rộng nó với các automation tùy chỉnh. Bạn cần Node 18+, một TypeScript setup hoạt động được, và Cursor Pro. Nếu chưa có Cursor Pro, các bước cấu hình trong bài này sẽ không xuất hiện trên giao diện của bạn. Chưa chắc Cursor có phù hợp không? Xem đánh giá Cursor 2026 của chúng tôi trước.
MCP là gì (phiên bản ngắn)
Model Context Protocol là một open standard để kết nối AI assistant với các khả năng bên ngoài. Hãy hình dung nó như USB-C cho AI tooling: một giao thức duy nhất mà bất kỳ host nào (Cursor, Claude Desktop, Zed) và bất kỳ server nào (code của bạn, database, REST API) đều có thể giao tiếp. Có ba primitive: Tools (function mà model có thể gọi), Resources (data source mà model có thể đọc), và Prompts (template prompt tái sử dụng). Bài này chỉ đề cập đến Tools — đây là primitive bạn sẽ dùng đến 90% thời gian.
Spec đầy đủ: modelcontextprotocol.io/docs. Muốn dùng MCP với Claude Code thay vì Cursor? Xem hướng dẫn MCP cho Claude Code.
Yêu cầu
- Node 18 trở lên (
node -vđể kiểm tra) - TypeScript 5.x (
npx tsc --version) - Cursor Pro
Bước 1: Tạo MCP server package
mkdir cursor-mcp-tools && cd cursor-mcp-tools
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --outDir dist
Thêm "type": "module" vào package.json — MCP SDK dùng ES modules.
{
"name": "cursor-mcp-tools",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Lỗi thường gặp: bỏ thiếu "type": "module" sẽ gây ra ERR_REQUIRE_ESM khi chạy. Output lỗi của Cursor rất ngắn gọn; đây là cái bẫy debug phổ biến nhất trong giờ đầu tiên.
Bước 2: Viết tool đầu tiên — search_files
Tạo src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
const server = new McpServer({
name: "cursor-mcp-tools",
version: "1.0.0",
});
server.tool(
"search_files",
"Search files in a directory for a text pattern",
{
directory: z.string().describe("Absolute path to search"),
pattern: z.string().describe("Text pattern to match"),
},
async ({ directory, pattern }) => {
try {
const entries = await readdir(directory, { withFileTypes: true });
const matches: string[] = [];
for (const entry of entries) {
if (!entry.isFile()) continue;
const filePath = join(directory, entry.name);
const content = await readFile(filePath, "utf-8").catch(() => "");
if (content.includes(pattern)) {
matches.push(entry.name);
}
}
return {
content: [
{
type: "text",
text:
matches.length > 0
? `Found in: ${matches.join(", ")}`
: "No matches found.",
},
],
};
} catch (err) {
return {
isError: true,
content: [{ type: "text", text: `Error: ${(err as Error).message}` }],
};
}
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Lỗi thường gặp: bất kỳ lệnh console.log hay console.error nào trong tool handler đều sẽ làm hỏng stdio transport — MCP dùng stdin/stdout làm kênh truyền thông điệp. Dùng process.stderr.write(...) nếu cần debug output. Lỗi này sinh ra các parse error khó hiểu trong Cursor và không có gì khác.
Build:
npm run build
Bạn sẽ thấy dist/index.js mà không có TypeScript error nào.
Bước 3: Đăng ký server trong Cursor
Tạo (hoặc chỉnh sửa) .cursor/mcp.json ở thư mục gốc project — hoặc ở ~/.cursor/mcp.json để dùng được trên tất cả các project:
{
"mcpServers": {
"cursor-mcp-tools": {
"command": "node",
"args": ["/absolute/path/to/cursor-mcp-tools/dist/index.js"]
}
}
}
Thay /absolute/path/to/cursor-mcp-tools bằng đường dẫn thực trên máy của bạn. Đường dẫn tương đối không hoạt động ở đây — Cursor spawn server process từ working directory của chính nó, không phải của bạn.
Khởi động lại Cursor hoàn toàn — không phải chỉ reload window (Cmd+Shift+P → Reload Window). MCP server được spawn khi khởi động ứng dụng. Reload window không spawn lại chúng. Hầu như ai cũng bị vấp ở điểm này lần đầu.
Sau khi khởi động lại, mở Cursor Chat và tìm Available Tools trong panel cài đặt model. search_files sẽ xuất hiện ở đó.
Lỗi thường gặp: nếu tool không xuất hiện, nguyên nhân phổ biến nhất là thiếu key mcpServers (key JSON ở cấp cao nhất là bắt buộc, dù có vẻ thừa). Kiểm tra lại .cursor/mcp.json có khớp đúng với cấu trúc trên không.
Bước 4: Gọi tool từ chat
Trong Cursor Chat, nhập:
Search /Users/yourname/projects/my-app for the string "TODO"
Cursor sẽ hiện prompt xác nhận trước khi gọi tool — đây là chủ ý. Hãy chấp nhận. Bạn sẽ thấy phản hồi của tool ngay trong chat: các tên file khớp, hoặc “No matches found.”
Model quyết định khi nào gọi tool dựa vào chuỗi description bạn truyền vào server.tool(...). Hãy viết description khớp với cách bạn diễn đạt yêu cầu bằng ngôn ngữ tự nhiên.
Bước 5: Thêm tool thứ hai — run_tests
Mở rộng src/index.ts trước lệnh gọi connect:
import { exec } from "node:child_process";
import { promisify } from "node:util";
const execAsync = promisify(exec);
server.tool(
"run_tests",
"Run the test suite for a project and return the output",
{
projectPath: z.string().describe("Absolute path to the project root"),
testCommand: z
.string()
.default("npm test")
.describe("Command to run tests (default: npm test)"),
},
async ({ projectPath, testCommand }) => {
try {
const { stdout, stderr } = await execAsync(testCommand, {
cwd: projectPath,
timeout: 60_000,
});
return {
content: [{ type: "text", text: stdout || stderr }],
};
} catch (err: any) {
return {
isError: true,
content: [
{
type: "text",
text: `Tests failed:\n${err.stdout ?? ""}\n${err.stderr ?? ""}`,
},
],
};
}
}
);
Flag isError: true rất quan trọng. Nó báo cho Cursor biết tool call đã thất bại — khiến model đọc output lỗi và phản hồi phù hợp, thay vì coi exit code khác 0 là kết quả thành công rỗng. Luôn đặt isError: true trong các catch block.
Build lại và khởi động lại Cursor sau khi thêm tool thứ hai.
Những điểm cần lưu ý
console.log làm hỏng transport. MCP dùng stdin/stdout làm message bus. Bất kỳ lệnh console.log nào — kể cả trong dependency — đều sẽ inject text vào stream và phá vỡ quá trình parsing. Dùng process.stderr.write(...) cho debug output, không phải console.*.
Thiếu key mcpServers. Cursor parse .cursor/mcp.json nghiêm ngặt. Key mcpServers ở cấp cao nhất phải có mặt. Một file JSON hoàn toàn hợp lệ nhưng thiếu key này sẽ không báo lỗi và không có tool nào được load.
npx -y cho server command. Nếu bạn chạy MCP server qua npx (ví dụ một package đã publish), thêm flag -y: "command": "npx", "args": ["-y", "some-mcp-server"]. Không có -y, npx dừng lại để xác nhận việc cài đặt — khiến stdio channel bị treo vĩnh viễn.
Đường dẫn tuyệt đối ở khắp nơi. Mảng args trong .cursor/mcp.json phải dùng đường dẫn tuyệt đối. ~/ không được expand. Đường dẫn tương đối ../ được resolve từ thư mục process của Cursor, không phải thư mục gốc project của bạn.
Cấu hình ESM module. MCP TypeScript SDK chỉ hỗ trợ ESM. tsconfig.json phải target NodeNext module resolution và package.json phải có "type": "module". Thiếu một trong hai sẽ sinh ra runtime import error trông giống như bug của SDK.
Những gì bạn đã xây dựng
| Hạng mục | Chi tiết |
|---|---|
| MCP server | TypeScript, hai tool (search_files, run_tests) |
| Đăng ký | .cursor/mcp.json, project-scoped |
| Thời gian đến tool đầu tiên hoạt động | ~20 phút |
| Những gì bạn có thêm | Cursor Chat có thể tìm kiếm filesystem và chạy test theo yêu cầu |
Từ đây, thêm nhiều tool hơn cho workflow của bạn — query database local, gọi internal API, đọc environment config. Pattern luôn giống nhau: server.tool(name, description, schema, handler). Description là thứ duy nhất model nhìn thấy khi quyết định có gọi tool hay không, vì vậy hãy viết nó như một function docstring.
MCP Inspector đầy đủ để debug server local: modelcontextprotocol.io/docs/tools/inspector.