Cài một công cụ nhận dạng giấy tờ vào trình soạn thảo chỉ mất một phút. Biết dùng nó vào việc gì sau đó thì lâu hơn. Bài viết này bỏ qua phần hướng dẫn cài đặt và đi thẳng vào ba việc mà lập trình viên thực sự giao cho Claude Code và Cursor khi có hộ chiếu liên quan:

  1. Biến một thư mục ảnh quét hộ chiếu thành tệp CSV, theo cách vẫn trụ được nếu bị sập giữa chừng.
  2. Đối chiếu một hộ chiếu với biểu mẫu đăng ký mà bạn đang xây dựng.
  3. Ghi lại kết quả quét một lần và dùng chúng làm fixture kiểm thử, để bộ kiểm thử của bạn không bao giờ gọi đến một dịch vụ thật.

Cả ba đều dùng máy chủ MCP của doc.cheap, gói @doc-cheap/mcp (MIT, Node 20 trở lên). Đây là blog của chính doc.cheap, API OCR hộ chiếu và căn cước đứng sau máy chủ đó, nên hãy cân nhắc các lựa chọn sản phẩm cho phù hợp. Bản thân các mẫu làm việc (một thư mục ảnh có rào chắn, một khóa cho mỗi tệp, một fixture ghi lại một lần) áp dụng được cho mọi công cụ cùng loại.

Máy chủ mang lại gì cho trình soạn thảo

Sáu công cụ, mỗi công cụ kèm những gợi ý mà client đọc trước khi quyết định có cần hỏi bạn trước hay không:

Công cụ Chức năng Có trừ tín dụng không?
scan_document Đọc hộ chiếu, thẻ căn cước hoặc giấy phép lái xe thành JSON có cấu trúc Chỉ khi nhận dạng được giấy tờ
check_balance Tín dụng còn lại và các bộ đếm của tháng này Không
search_docs Tìm trong tài liệu API đi kèm máy chủ, ngoại tuyến Không
list_scans Liệt kê các lượt quét đã lưu của tài khoản, mới nhất trước Không
get_scan Đọc lại một lượt quét đã lưu theo id Không
delete_scan Xóa vĩnh viễn một lượt quét đã lưu Không

scan_document nhận ảnh qua image_base64, image_url hoặc, trên máy chủ cục bộ, image_path, cùng các tùy chọn expect_country, return_portrait, retain_hours, reference và idempotency_key. Năm tùy chọn này gánh phần lớn các quy trình bên dưới.

Khi chưa đặt khóa, máy chủ dùng khóa sandbox công khai: tổng cộng 10 giấy tờ được nhận dạng miễn phí cho mỗi địa chỉ, và tối đa 10 yêu cầu mỗi giờ cho mỗi địa chỉ. Như vậy là đủ cho quy trình 2 và 3. Quy trình 1 cần khóa riêng của bạn; đăng ký tài khoản sẽ có 100 lượt nhận dạng miễn phí mỗi tháng.

Nếu bạn chưa từng kết nối máy chủ MCP với một agent, bài Để agent đọc giấy tờ tùy thân qua MCP trình bày kỹ hơn những điều cơ bản và các câu hỏi về quyền riêng tư.

Quy trình 1: thư mục ảnh quét thành CSV, trong Claude Code

Công việc. Có người đưa bạn một thư mục ảnh quét hộ chiếu (hồ sơ khách tồn đọng của một khách sạn, danh sách người tham dự một sự kiện, một đợt chuyển đổi từ kho lưu trữ giấy) và muốn có một bảng tính: mỗi tệp một dòng, các trường cần thiết, và một ghi chú rõ ràng cho mỗi tệp không đọc được.

Thiết lập. Đặt một tệp .mcp.json ở thư mục gốc của dự án. Claude Code đọc nó như danh sách máy chủ riêng của dự án, nên cấu hình đi cùng kho mã:

{
  "mcpServers": {
    "doc-cheap": {
      "command": "npx",
      "args": ["-y", "@doc-cheap/mcp"],
      "env": {
        "DOC_CHEAP_API_KEY": "${DOC_CHEAP_API_KEY:-}",
        "DOC_CHEAP_IMAGE_ROOT": "/Users/you/guest-backlog/scans"
      }
    }
  }
}

Lần đầu Claude Code khởi động trong thư mục đó, nó sẽ hỏi bạn phê duyệt máy chủ trong .mcp.json.

Hai dòng làm nên chuyện:

  • DOC_CHEAP_API_KEY được lấy từ shell của bạn, nên không có khóa nào bị commit. Khi biến này chưa được đặt, giá trị để trống và máy chủ quay về dùng khóa sandbox.
  • DOC_CHEAP_IMAGE_ROOT là thư mục duy nhất mà máy chủ được phép đọc tệp. Không có nó, image_path bị từ chối ngay. Có nó, đường dẫn được phân giải qua các liên kết tượng trưng trước, và mọi thứ thoát ra ngoài thư mục (một .., một liên kết trỏ ra ngoài) đều bị từ chối. Một image_path tương đối được tính từ thư mục này, nên agent có thể chỉ truyền tên tệp.

Rào chắn đó ở đây quan trọng hơn bất cứ đâu. Tên tệp do agent đưa ra, và agent đang đọc một thư mục không phải do bạn tạo. Ảnh cũng không bao giờ đi qua cuộc hội thoại: máy chủ đọc tệp và gửi thẳng đến API.

Câu lệnh.

Gọi check_balance trước. Sau đó, với mỗi tệp JPEG và PNG trong thư mục ảnh quét, gọi scan_document với image_path là tên tệp, reference là tên tệp, idempotency_key là "backlog-" cộng với SHA-256 của tệp, return_portrait là false và retain_hours là 24. Ghi backlog.csv với các cột file, status, billed, kind, country, number, surname, given_names, birth_date, expiry_date, is_expired, mrz_status. Với mọi trạng thái khác recognized, để trống các cột dữ liệu. Cuối cùng, liệt kê các tệp không được nhận dạng và lý do.

Vì sao có từng tùy chọn:

  • reference được trả lại trong meta.reference của kết quả và hiện trên mỗi dòng của list_scans, nên mỗi lượt quét đã lưu đều truy ngược được về tệp của nó mà không cần mở ra.
  • idempotency_key là thứ giúp lô xử lý có thể tiếp tục an toàn. Với khóa thật, một lần gọi lặp lại cùng khóa sẽ trả về kết quả đầu tiên thay vì nhận dạng và tính phí lần nữa. Nếu phiên làm việc dừng ở tệp 140 trên 300, hãy chạy lại đúng câu lệnh đó: 139 tệp đầu quay về dưới dạng phát lại miễn phí, và công việc tiếp tục từ đó. Chính khóa được dựng từ mã băm của tệp, chứ không phải một UUID ngẫu nhiên, mới cho phép một phiên mới tạo lại nó. Claude Code có thể tính bằng shasum -a 256.
  • retain_hours: 24 là sự đánh đổi mà hướng dẫn về idempotency giải thích. Một lần phát lại trả về kết quả đã lưu, nên lượt quét với retain_hours: 0 không thể phát lại: một lần thử lại dưới khóa của nó trong vòng 24 giờ sẽ bị từ chối với idempotency_replay_unavailable. Khung một ngày giữ cho các lần thử lại miễn phí mà ngày hôm sau vẫn không còn gì được lưu.
  • return_portrait: false bỏ phần ảnh chân dung cắt từ giấy tờ, thứ mà tệp CSV không cần đến.

Kết quả trả về. status của mỗi dòng là một trong recognized, no_document_found, unreadable, unsupported_document hoặc rejected, và billed cho biết tệp đó có tiêu tốn tín dụng hay không. Chỉ recognized bị tính phí, nên một thư mục có hai mươi ảnh bị mờ sẽ tốn ít hơn số tệp hai mươi tín dụng. Cột billed cũng chính là bản đếm của bạn về những gì lô xử lý đã tiêu, không cần đối chiếu hóa đơn.

Thêm một thói quen. Tệp CSV giờ chứa dữ liệu cá nhân. Hãy thêm nó vào .gitignore trước khi agent ghi ra, đừng để sau.

Quy trình 2: đối chiếu một hộ chiếu khi đang xây dựng biểu mẫu, trong Cursor

Công việc. Bạn đang xây dựng biểu mẫu đăng ký hoặc check-in, và muốn xem một lượt quét thật trả về gì trước khi quyết định tên trường, định dạng và cách kiểm tra hợp lệ.

Thiết lập. Để xem nhanh, Cursor có thể dùng máy chủ được lưu trữ sẵn, không cần cài đặt. Trong .cursor/mcp.json của dự án:

{
  "mcpServers": {
    "doc-cheap": {
      "url": "https://mcp.doc.cheap/mcp"
    }
  }
}

Khi không có header chứa khóa, máy chủ lưu trữ sẵn dùng khóa sandbox. Nó không thấy được tệp trên máy bạn, nên ở đó scan_document nhận ảnh dưới dạng image_base64 hoặc một image_url công khai bắt đầu bằng https:. Nếu mẫu giấy tờ của bạn là tệp cục bộ, hãy chạy máy chủ trên máy của bạn. Cursor viết biến theo cú pháp riêng, ${workspaceFolder} cho thư mục dự án và ${env:NAME} cho biến shell, nên mục của quy trình 1 không được sao chép nguyên vẹn:

{
  "mcpServers": {
    "doc-cheap": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@doc-cheap/mcp"],
      "env": {
        "DOC_CHEAP_IMAGE_ROOT": "${workspaceFolder}/specimens"
      }
    }
  }
}

Khi không có dòng DOC_CHEAP_API_KEY, mục này cũng dùng khóa sandbox, và máy chủ chỉ đọc ảnh từ thư mục specimens của dự án. Ở đây hãy dùng giấy tờ tổng hợp hoặc giấy tờ mẫu, đừng bao giờ dùng hộ chiếu của chính bạn.

Câu lệnh.

Quét td3-specimen.jpg bằng scan_document. So sánh kết quả với các trường trong components/OnboardingForm.tsx và đề xuất thay đổi để mọi trường của biểu mẫu đều được điền từ kết quả quét. Dùng search_docs cho bất kỳ trường nào bạn chưa chắc.

Những gì phép so sánh thường làm lộ ra, và cấu trúc kết quả giải đáp ra sao:

  • Họ và tên được tách sẵn. holder.surname và holder.given_names là hai trường riêng, còn holder.full_name ghép chúng lại. Một ô "Họ tên đầy đủ" duy nhất sẽ làm mất phần tách mà bộ phận hậu cần của bạn sẽ cần về sau.
  • Ngày tháng đã ở dạng ISO YYYY-MM-DD. holder.birth_date và document.expiry_date đưa thẳng vào <input type="date">, vốn dùng cùng định dạng cho giá trị. Không phải phân tích DD.MM.YYYY hay ngày sáu chữ số riêng của MRZ.
  • Quốc gia đến dưới dạng mã kèm chi tiết. holder.nationality là mã ba chữ cái của giấy tờ, và holder.nationality_detail bổ sung dạng ISO alpha-3 và alpha-2. Nếu bộ chọn quốc gia của bạn dùng mã hai chữ cái, hãy ánh xạ từ đối tượng chi tiết thay vì tự viết bảng riêng.
  • Hạn sử dụng được tính sẵn. document.is_expired và document.days_remaining (âm khi đã hết hạn) cho phép biểu mẫu cảnh báo hộ chiếu hết hạn mà không cần tính toán ngày tháng.
  • Mọi giá trị đều có thể là null. Mỗi khóa luôn có mặt, và giá trị không xác định là null, không bao giờ là khóa bị thiếu. Biểu mẫu nào giả định mọi trường đều có sẽ hỏng ngay ở cuốn hộ chiếu sờn cũ đầu tiên.
  • Kết luận về MRZ tách riêng khỏi các trường. mrz.status là passed, failed hoặc absent. Nó cho biết vùng đọc máy có được đọc và các chữ số kiểm tra có khớp hay không; nó không nói giấy tờ là thật. Nhóm authenticity của kết quả ghi not_checked.

search_docs rất đáng dùng trong vòng lặp này. Nó đọc tài liệu đi kèm máy chủ, chạy ngoại tuyến và không tốn gì, nên những câu như "unsupported_document nghĩa là gì" hay "thẻ căn cước có những trường nào" sẽ nhận được câu trả lời theo tài liệu thay vì phỏng đoán.

Quy trình 3: ghi fixture kiểm thử một lần

Công việc. Mã của bạn gọi API, nhưng các bài kiểm thử của nó thì không nên. Một bộ kiểm thử gửi hộ chiếu đến dịch vụ thật ở mỗi lần chạy sẽ chậm, cần khóa trong CI, và trên sandbox sẽ sớm chạm giới hạn: 10 yêu cầu mỗi giờ, và lỗi từ chối document_repeated khi cùng một giấy tờ được gửi quá nhiều lần.

Cách giải quyết là ghi lại mỗi kết quả mà mã của bạn phải xử lý một lần, lưu thành JSON, rồi cho các bài kiểm thử chạy trên những tệp đó.

Câu lệnh, trong Claude Code hoặc Cursor đều được:

Quét td3-specimen.jpg và blank-page.png bằng scan_document và lưu mỗi kết quả có cấu trúc thành test/fixtures/scan-.json. Thay mọi giá trị cá nhân (tên, số, ngày sinh, các dòng MRZ) bằng một giá trị giữ chỗ cùng kiểu, và xóa các data URL trong đối tượng images.

Kết quả có cấu trúc của công cụ chính là đối tượng Scan mà API HTTP trả về, nên fixture khớp với những gì mã của bạn sẽ nhận trong môi trường production. Một fixture đã che dữ liệu trông như sau:

{
  "meta": { "status": "recognized", "billed": false, "reference": null },
  "document": { "kind": "passport", "country": "<ICAO3>", "number": "<NUMBER>", "expiry_date": "<YYYY-MM-DD>", "is_expired": false, "days_remaining": 0 },
  "holder": { "surname": "<SURNAME>", "given_names": "<GIVEN NAMES>", "birth_date": "<YYYY-MM-DD>", "nationality": "<ICAO3>" },
  "mrz": { "status": "passed", "reason": null, "lines": ["P<…", "…"], "text": "P<…" }
}

(Đã rút gọn ở đây; hãy giữ mọi khóa có trong kết quả thật, để bài kiểm thử thất bại khi mã của bạn đọc một khóa bị thiếu.)

Ba chi tiết giúp fixture trung thực:

  • Ghi lại cả các trường hợp thất bại. Một trang trắng cho ra no_document_found với billed: false và các nhóm dữ liệu là null. Chính tệp đó bắt được đoạn mã giả định rằng lượt quét nào cũng có người mang giấy tờ.
  • Trên sandbox, billed là dự báo. Ở đó không có gì bị tính phí, và cờ này cho biết cùng lượt quét đó có bị tính phí với khóa thật hay không. Đó đúng là điều mà một bài kiểm thử thanh toán cần khẳng định.
  • Với khóa thật, hãy đọc lại thay vì quét lần nữa. list_scans tìm một lượt quét trước đó theo reference hoặc thời điểm, và get_scan trả nó về đúng như lần đầu, không nhận dạng lại ảnh và không tính phí. Hai điểm khác: mọi ô trong images đều là null và quality ghi not_checked, vì ảnh cắt không bao giờ được lưu.

Nếu bạn muốn ghi từ một script thay vì từ trình soạn thảo, cùng lời gọi đó qua HTTP thuần là:

curl -X POST https://api.doc.cheap/v1/scans \
  -H "Authorization: Bearer sk_sandbox_public" \
  -H "Content-Type: application/json" \
  -d "{\"image\": \"$(base64 < specimens/td3-specimen.jpg | tr -d '\n')\"}" \
  -o test/fixtures/scan-recognized.json

Hãy che dữ liệu trong tệp trước khi commit.

Mỗi quy trình cần gì

Khóa Máy chủ Ảnh đến API từ
Thư mục thành CSV Khóa riêng (khi vượt 10 giấy tờ) Cục bộ, npx Thư mục có rào chắn
Đối chiếu biểu mẫu Sandbox là đủ Lưu trữ sẵn hoặc cục bộ Một URL hoặc thư mục có rào chắn
Fixture kiểm thử Sandbox là đủ Loại nào cũng được Thư mục có rào chắn, một lần

Cấu hình cho từng trình soạn thảo, gồm cả Windsurf, VS Code, Gemini CLI và Kiro, có trên trang máy chủ MCP OCR cho Claude và Cursor và trong hướng dẫn máy chủ MCP. Nếu bạn vẫn đang chọn dịch vụ, bảng so sánh API OCR hộ chiếu đặt doc.cheap cạnh các dịch vụ khác dựa trên những gì mỗi bên công bố.

Nếu một trong các quy trình này gặp lỗi trong trình soạn thảo của bạn, hoặc bạn có quy trình thứ tư đáng viết thành bài, hãy viết cho admin@doc.cheap.

Một câu hỏi cho bạn: bạn thường giao cho agent trong trình soạn thảo việc gì hơn – từng giấy tờ một, hay cả một thư mục? Hãy cho chúng tôi biết trong phần bình luận bên dưới.