تثبيت أداة للتعرّف على المستندات في محرر الشيفرة يستغرق دقيقة. أما معرفة ما تفعله بها بعد ذلك فتستغرق وقتًا أطول. يتجاوز هذا المقال جولة الإعداد وينتقل مباشرة إلى ثلاث مهام يأتي بها المطوّرون فعلًا إلى Claude Code وCursor عندما تكون جوازات السفر جزءًا من العمل:

  1. تحويل مجلد من مسوحات جوازات السفر إلى ملف CSV، بطريقة تصمد أمام انقطاع العمل في منتصفه.
  2. مطابقة جواز سفر واحد مع نموذج التسجيل الذي تبنيه.
  3. تسجيل نتائج المسح مرة واحدة واستخدامها بيانات اختبار ثابتة (fixtures)، حتى لا تستدعي مجموعة اختباراتك خدمة حيّة أبدًا.

تستخدم المهام الثلاث خادم MCP من doc.cheap، أي الحزمة @doc-cheap/mcp (برخصة MIT، وتتطلب Node 20 أو أحدث). هذه مدوّنة doc.cheap نفسها، أي واجهة OCR لجوازات السفر وبطاقات الهوية التي يقف خلفها ذلك الخادم، فقيّم خيارات المنتج على هذا الأساس. أما الأنماط نفسها (مجلد صور محصور، ومفتاح لكل ملف، وبيانات اختبار تُسجَّل مرة واحدة) فتنطبق على أي أداة من هذا النوع.

ما الذي يقدّمه الخادم للمحرر

ست أدوات، لكل منها التلميحات التي يقرؤها العميل قبل أن يقرّر هل يسألك أولًا:

الأداة ما تفعله هل تستهلك رصيدًا؟
scan_document تقرأ جواز سفر أو بطاقة هوية أو رخصة قيادة وتعيدها بصيغة JSON منظّمة فقط عند التعرّف على مستند
check_balance الرصيد المتبقي وعدّادات هذا الشهر لا
search_docs تبحث في توثيق الواجهة المرفق مع الخادم، دون اتصال لا
list_scans تعرض المسوحات المحفوظة في الحساب، الأحدث أولًا لا
get_scan تعيد قراءة مسح محفوظ بمعرّفه لا
delete_scan تحذف مسحًا محفوظًا نهائيًا لا

تأخذ scan_document الصورة بصيغة image_base64 أو image_url، أو image_path على الخادم المحلي، إضافة إلى الخيارات الاختيارية expect_country وreturn_portrait وretain_hours وreference وidempotency_key. هذه الخيارات الخمسة هي التي تحمل معظم سير العمل التالية.

إذا لم يُضبط مفتاح، يستخدم الخادم مفتاح البيئة التجريبية (sandbox) العام: 10 مستندات مُتعرَّف عليها مجانًا لكل عنوان إجمالًا، و10 طلبات على الأكثر لكل عنوان في الساعة. هذا يكفي لسير العمل 2 و3. أما سير العمل 1 فيحتاج مفتاحًا خاصًا بك؛ والتسجيل يمنحك 100 عملية تعرّف مجانية كل شهر.

إذا لم يسبق لك ربط خادم MCP بوكيل، فمقال كيف تجعل وكيلًا يقرأ مستندات الهوية عبر MCP يشرح الأساسيات ويتناول أسئلة الخصوصية بتفصيل أكبر.

سير العمل 1: مجلد مسوحات إلى ملف CSV في Claude Code

المهمة. يسلّمك أحدهم مجلدًا من مسوحات جوازات السفر (نزلاء فندق متراكمون، أو قائمة حضور فعالية، أو نقل من أرشيف ورقي) ويريد جدولًا: صفًا لكل ملف، والحقول المهمة، وملاحظة واضحة لكل ملف تعذّرت قراءته.

الإعداد. ضع ملف .mcp.json في جذر المشروع. يقرؤه Claude Code على أنه قائمة الخوادم الخاصة بالمشروع، فتنتقل الإعدادات مع المستودع:

{
  "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"
      }
    }
  }
}

في أول مرة يبدأ فيها Claude Code داخل ذلك المجلد، يطلب منك الموافقة على الخادم الوارد في .mcp.json.

سطران يقومان بالعمل:

  • DOC_CHEAP_API_KEY يُؤخذ من الطرفية لديك، فلا يدخل أي مفتاح في commit. وإذا لم يكن المتغيّر مضبوطًا تكون القيمة فارغة ويعود الخادم إلى مفتاح البيئة التجريبية.
  • DOC_CHEAP_IMAGE_ROOT هو المجلد الوحيد الذي يُسمح للخادم بقراءة الملفات منه. من دونه يُرفض image_path رفضًا تامًا. ومعه يُحلّ المسار أولًا عبر الروابط الرمزية، ويُرفض كل ما يخرج من المجلد (مثل .. أو رابط يشير إلى الخارج). ويُحتسب image_path النسبي من هذا المجلد، فيستطيع الوكيل تمرير أسماء الملفات وحدها.

هذا الحصر مهم هنا أكثر من أي مكان آخر. فأسماء الملفات تأتي من الوكيل، والوكيل يقرأ مجلدًا لم تنشئه أنت. كما أن الصور لا تمرّ أبدًا عبر المحادثة: يقرأ الخادم الملف ويرسله مباشرة إلى الواجهة.

التعليمات.

استدعِ check_balance أولًا. ثم لكل ملف JPEG وPNG في مجلد المسوحات، استدعِ scan_document مع ضبط image_path على اسم الملف، وreference على اسم الملف، وidempotency_key على "backlog-" متبوعًا بقيمة SHA-256 للملف، وreturn_portrait على false وretain_hours على 24. اكتب الملف backlog.csv بالأعمدة file وstatus وbilled وkind وcountry وnumber وsurname وgiven_names وbirth_date وexpiry_date وis_expired وmrz_status. لأي status غير recognized اترك أعمدة البيانات فارغة. وفي النهاية اذكر الملفات التي لم يُتعرّف عليها وسبب ذلك.

لماذا وُضع كل خيار:

  • reference يُعاد في الحقل meta.reference من النتيجة ويظهر في كل صف من list_scans، فيمكن ربط كل مسح محفوظ بملفه دون فتحه.
  • idempotency_key هو ما يجعل استئناف الدفعة آمنًا. على مفتاح حيّ، يعيد التكرار بالمفتاح نفسه النتيجة الأولى بدل التعرّف والخصم من جديد. إذا انقطعت الجلسة عند الملف 140 من 300، فشغّل التعليمات نفسها مرة أخرى: تعود الملفات الـ139 الأولى إعادةً دون تكلفة، ويستمر العمل من هناك. والمفتاح المبني من بصمة (hash) الملف، لا من UUID عشوائي، هو ما يسمح لجلسة جديدة بإعادة إنتاجه. يستطيع Claude Code حسابه بالأمر shasum -a 256.
  • retain_hours: 24 هو المفاضلة التي يشرحها دليل التكرار الآمن (idempotency). فالإعادة تُرجع النتيجة المحفوظة، ولذلك لا يمكن إعادة مسح أُجري مع retain_hours: 0: أي محاولة جديدة بمفتاحه خلال 24 ساعة تُرفض بالرمز idempotency_replay_unavailable. نافذة يوم واحد تُبقي المحاولات المتكررة مجانية، ولا تترك شيئًا محفوظًا في اليوم التالي.
  • return_portrait: false يستبعد قصاصة صورة حامل الجواز، ولا حاجة لملف CSV بها.

ما الذي يعود. قيمة status في كل صف واحدة من recognized أو no_document_found أو unreadable أو unsupported_document أو rejected، ويبيّن billed هل استهلك ذلك الملف رصيدًا. وحدها recognized تُحتسب، فالمجلد الذي فيه عشرون صورة ضبابية يكلّف عشرين رصيدًا أقل من عدد ملفاته. وعمود billed هو أيضًا إحصاؤك الخاص لما أنفقته الدفعة، دون فاتورة تحتاج إلى تسوية.

عادة أخرى. أصبح ملف CSV يحتوي بيانات شخصية. أضفه إلى .gitignore قبل أن يكتبه الوكيل، لا بعده.

سير العمل 2: التحقق من جواز سفر واحد أثناء بناء نموذج في Cursor

المهمة. تبني نموذج تسجيل أو نموذج تسجيل وصول، وتريد أن ترى ما يعيده مسح حقيقي قبل أن تقرّر أسماء الحقول وصيغها وقواعد التحقق منها.

الإعداد. لنظرة سريعة، يستطيع Cursor استخدام الخادم المستضاف الذي لا يحتاج إلى تثبيت. في ملف .cursor/mcp.json الخاص بالمشروع:

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

من دون ترويسة تحمل مفتاحًا، يستخدم الخادم المستضاف مفتاح البيئة التجريبية. وهو لا يرى الملفات على جهازك، لذلك تأخذ scan_document هناك الصورة بصيغة image_base64 أو عنوان image_url عام يبدأ بـhttps:. إذا كانت عيّناتك ملفات محلية، فشغّل الخادم محليًا بدلًا من ذلك. يكتب Cursor المتغيّرات بصيغته الخاصة، ${workspaceFolder} لمجلد المشروع و${env:NAME} لمتغيّر من الصدفة، لذلك لا يُنسخ مُدخل سير العمل 1 كما هو:

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

من دون سطر DOC_CHEAP_API_KEY يستخدم هذا المُدخل أيضًا مفتاح البيئة التجريبية، ولا يقرأ الخادم الصور إلا من مجلد specimens في المشروع. استخدم هنا مستندات اصطناعية أو نماذج عيّنة، وليس جوازك أبدًا.

التعليمات.

امسح td3-specimen.jpg باستخدام scan_document. قارن النتيجة بالحقول في components/OnboardingForm.tsx واقترح تعديلات بحيث يُملأ كل حقل في النموذج من المسح. استخدم search_docs لأي حقل لست متأكدًا منه.

ما تكشفه المقارنة عادة، وما يجيب عنه شكل النتيجة:

  • الأسماء تأتي منفصلة. holder.surname وholder.given_names حقلان منفصلان، وholder.full_name يجمعهما. حقل واحد باسم "الاسم الكامل" يُضيّع هذا الفصل الذي سيحتاجه فريقك الإداري لاحقًا.
  • التواريخ بصيغة ISO YYYY-MM-DD مسبقًا. يدخل holder.birth_date وdocument.expiry_date مباشرة في <input type="date">، الذي تستخدم قيمته الصيغة نفسها. لا حاجة إلى تحليل DD.MM.YYYY ولا تواريخ MRZ المكوّنة من ست خانات.
  • الدول تأتي رموزًا مع تفاصيلها. holder.nationality هو الرمز الثلاثي المطبوع في المستند، وholder.nationality_detail يضيف صيغتي ISO alpha-3 وalpha-2. إذا كان منتقي الدولة لديك يعتمد الرموز الثنائية، فاربط القيم من كائن التفاصيل بدل كتابة جدول خاص بك.
  • انتهاء الصلاحية محسوب لك. يتيح document.is_expired وdocument.days_remaining (سالب بعد انتهاء الصلاحية) للنموذج أن ينبّه إلى جواز منتهي الصلاحية دون أي حساب للتواريخ.
  • أي قيمة قد تكون null. كل مفتاح موجود دائمًا، والقيمة غير المعروفة تكون null، لا مفتاحًا مفقودًا. النموذج الذي يفترض وصول كل حقل سينكسر عند أول جواز بالٍ.
  • حكم MRZ منفصل عن الحقول. قيمة mrz.status هي passed أو failed أو absent. وهي تخبرك هل قُرئت المنطقة المقروءة آليًا وهل تتطابق أرقام التحقق فيها؛ لكنها لا تقول إن المستند أصلي. فمجموعة authenticity في النتيجة تُظهر not_checked.

يستحق search_docs الاستخدام في هذه الحلقة. فهو يقرأ التوثيق المرفق مع الخادم، ويعمل دون اتصال، ولا يكلّف شيئًا، فيحصل سؤال مثل "ماذا تعني unsupported_document" أو "ما الحقول الموجودة في بطاقة الهوية" على الإجابة الموثّقة بدل التخمين.

سير العمل 3: تسجيل بيانات الاختبار مرة واحدة

المهمة. شيفرتك تستدعي الواجهة، أما اختباراتها فلا ينبغي أن تفعل. مجموعة اختبارات ترسل جواز سفر إلى خدمة حيّة في كل تشغيل تكون بطيئة، وتحتاج مفتاحًا في CI، وعلى البيئة التجريبية تصطدم سريعًا بحدودها: 10 طلبات في الساعة، ورفض document_repeated للمستند نفسه إذا أُرسل مرات كثيرة.

الحل أن تسجّل مرة واحدة كل نتيجة يجب أن تتعامل معها شيفرتك، وتحفظها بصيغة JSON، وتشغّل الاختبارات على هذه الملفات.

التعليمات، في Claude Code أو Cursor:

امسح td3-specimen.jpg وblank-page.png باستخدام scan_document واحفظ كل نتيجة منظّمة في test/fixtures/scan-.json. استبدل كل قيمة شخصية (الأسماء والأرقام وتواريخ الميلاد وأسطر MRZ) بعنصر نائب من النوع نفسه، واحذف عناوين data URL من الكائن images.

النتيجة المنظّمة للأداة هي كائن Scan نفسه الذي تعيده واجهة HTTP، فتطابق بيانات الاختبار ما ستتلقاه شيفرتك في بيئة الإنتاج. هكذا تبدو بيانات اختبار مُقنَّعة:

{
  "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<…" }
}

(اختُصرت هنا؛ احتفظ بكل مفتاح موجود في النتيجة الحقيقية، حتى يفشل الاختبار عندما تقرأ شيفرتك مفتاحًا غير موجود.)

ثلاث تفاصيل تجعل بيانات الاختبار صادقة:

  • سجّل حالات الفشل أيضًا. الصفحة الفارغة تعطيك no_document_found مع billed: false ومجموعات البيانات بقيمة null. هذا الملف هو الذي يكشف الشيفرة التي تفترض أن للمسح حاملًا دائمًا.
  • على البيئة التجريبية، billed توقّع. لا يُخصم شيء هناك، ويبيّن هذا العَلَم هل كان المسح نفسه سيُحتسب على مفتاح حيّ. وهذا بالضبط ما يحتاج اختبار الفوترة إلى التحقق منه.
  • على مفتاح حيّ، أعد القراءة بدل المسح من جديد. تجد list_scans مسحًا سابقًا بمرجعه أو بوقته، وتعيده get_scan كما أُعيد أول مرة، دون التعرّف على الصورة من جديد ودون خصم. هناك فرقان: كل خانة في images تكون null، وquality تُظهر not_checked، لأن قصاصات الصور لا تُحفظ أبدًا.

إذا كنت تفضّل التسجيل من سكربت بدل المحرر، فهذا الاستدعاء نفسه عبر HTTP مباشرة:

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

قنّع الملف قبل أن تضيفه في commit.

ما يحتاجه كل سير عمل

المفتاح الخادم من أين تصل الصور إلى الواجهة
مجلد إلى CSV مفتاحك الخاص (بعد 10 مستندات) محلي، npx المجلد المحصور
التحقق من النموذج البيئة التجريبية تكفي مستضاف أو محلي عنوان URL أو المجلد المحصور
بيانات الاختبار البيئة التجريبية تكفي أيّ منهما المجلد المحصور، مرة واحدة

إعدادات كل محرر، بما فيها Windsurf وVS Code وGemini CLI وKiro، موجودة في صفحة خادم OCR MCP لـClaude وCursor وفي دليل خادم MCP. وإذا كنت لا تزال تختار خدمة، فإن مقارنة واجهات OCR لجوازات السفر تضع doc.cheap إلى جانب الخدمات الأخرى وفق ما تنشره كل منها.

إذا تعطّل أحد سير العمل هذه في محررك، أو كان لديك سير عمل رابع يستحق الكتابة عنه، فراسلنا على admin@doc.cheap.

سؤال لك: ما الذي تسلّمه لوكيل محررك في أغلب الأحيان – مستندًا واحدًا في كل مرة أم مجلدًا كاملًا؟ أخبرنا في التعليقات أدناه.