Skip to content
Merged
122 changes: 121 additions & 1 deletion docs/mcp-server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,85 @@ OAuth 2.1) וגם מול **Claude Code / Claude Desktop** (טוקן אישי).
בו — לא ב-mirror ולא בריפו ב-GitHub. כתוב כאן כדי שאיש לא יסיק אחרת
משם הכלי.

.. _mcp-limits:

הקבועים, התקרות ואוצר המילים של התשובה
---------------------------------------

המספרים כאן חיים בקוד ונאכפים בו. ההפניה היא לשם המודול ולשם הקבוע ולא
למספר שורה, כדי שהטבלה לא תצביע למקום אחר אחרי הריפקטור הבא.

.. list-table::
:header-rows: 1
:widths: 34 16 50

* - הקבוע
- הערך
- מה הוא מגביל
* - ``MAX_FILE_SIZE_FOR_DISPLAY``
- 512,000
- קריאה מלאה של קובץ. יושב ב-``services/git_mirror_service.py``
ומשותף עם הוובאפ
* - ``RANGE_READ_MAX_BYTES``
- 10,485,760
- קריאת טווח **ואאוטליין**, ב-``mcp_server/repo_backend.py``
* - ``OUTPUT_BYTE_BUDGET``
- 256,000
- תקציב הבתים של תשובה אחת
* - ``TREE_PER_PAGE_DEFAULT`` · ``TREE_PER_PAGE_MAX``
- 200 · 1000
- עמוד בעץ הקבצים
* - ``OUTLINE_PER_PAGE_DEFAULT`` · ``OUTLINE_PER_PAGE_MAX``
- 100 · 500
- סימבולים בעמוד באאוטליין. קבועים משלו ולא מיחזור של ``TREE_*``,
כי רשומת סימבול שוקלת יותר מנתיב
* - ``REPOS_LIMIT_DEFAULT`` · ``REPOS_LIMIT_MAX``
- 50 · 200
- ריפואים ב-``codekeeper_list_repos``
* - ``SEARCH_RESULTS_DEFAULT`` · ``SEARCH_RESULTS_MAX``
- 50 · 100
- פגיעות ב-``codekeeper_search_repo``
* - ``CONTEXT_LINES_MAX``
- 10
- שורות ההקשר סביב פגיעה בחיפוש
* - ``MAX_SYMBOLS``
- 50,000
- סימבולים לקובץ באאוטליין. יושב ב-``mcp_server/outline_scanners/_ceiling.py``

כל קבוע שהטבלה אינה נוקבת במודול שלו יושב ב-``mcp_server/repo_handlers.py``,
ליד ה-handler שאוכף אותו.

.. warning::

**ערך מעל התקרה נצמד בשקט, ואינו נדחה.** ``per_page=1000`` באאוטליין
מקבל 500, ואין בתשובה שדה שמצהיר שזה קרה — מה שכן יש הוא ``per_page``
שחוזר בתשובה, והוא **הערך שנאכף**, ולכן זה מה שכדאי להשוות אליו ולא
למה שנשלח. ``per_page=0`` נצמד ל-1 ולא לברירת המחדל.

**ומה שאינו מספר שלם אינו מגיע להצמדה בכלל — הוא נדחה בסכימה.** נמדד
מול הסכימה של הכלי: ערך עם שבר (``2.5``), ``null``, ומחרוזת שאינה
מספר נדחים **לפני** שהכלי רץ; מחרוזת מספרית (``"250"``) ומספר עשרוני
שלם (``2.0``) עוברים ומומרים. **ובוליאני עובר ונעשה 1**, בלי שגיאה:
``per_page=true`` מחזיר עמוד של רשומה אחת. זו אותה המרה ש-``lines``
חוסם ב-``strict`` — ראו את ההערה שם — וכאן היא אינה חסומה.

ההצמדה היא מדיניות מוצהרת (``_clamp`` ב-``mcp_server/handlers.py``)
ולא מקריות: דחייה על כל ערך מחוץ לטווח הייתה מכשילה קריאה שלמה בגלל
פרמטר עימוד. התקרה על הגודל דווקא **כן** דוחה, וההבדל מכוון — עמוד
שאינו נכנס בתקציב הבתים חוזר כ-``page_too_large`` ולא נחתך בשקט.

**אוצר המילים של התשובה, ושתי המשפחות אינן מתערבבות.** ``status`` מתאר
הצלחה, ``error`` מתאר סירוב, וב**תשובת כלי** ``error`` מגיע תמיד עם
``"ok": false``. תשובת HTTP של השרת עצמו אינה תשובת כלי ואינה נושאת את
השדה — ``401`` על טוקן פסול, למשל, הוא ``{"error": "invalid_token"}``
לבדו, עם הכותרת ``WWW-Authenticate``. ל-``status`` יש בתשובת
כלי רשימה סגורה: ``ok``, ``outline``, ``binary``, ``too_large``
ו-``no_outline``. ל-``error`` אין רשימה כזאת כאן **בכוונה** — הערכים
נגזרים מהכלי שסירב (``path_denied``, ``outline_and_lines``,
``page_too_large``, ``range_out_of_bounds`` ועוד רבים), ורשימה חלקית
שנראית סגורה גרועה מאין רשימה. ערך ``status`` חדש שיתווסף לקוד מתווסף
גם כאן.

אימות והרשאות
--------------

Expand Down Expand Up @@ -575,7 +654,8 @@ Claude Desktop
**מה שאומר אם יש עוד עמודים הוא ``total`` מול ``page`` ו-``per_page``.**

**מגבלת הגודל.** אאוטליין נשפט מול אותה תקרה של קריאת טווח — **10MB**, ולא
500KB — כי הוא קורא את הקובץ ומחזיר פלט זעיר. ראו :ref:`mcp-line-range`.
500KB — כי הוא קורא את הקובץ ומחזיר פלט זעיר. ראו :ref:`mcp-line-range`, והמספרים
עצמם — כולל תקרות העימוד — ב-:ref:`mcp-limits`.

**``lines`` ו-``outline`` אינם מצטברים.** קריאה שמעבירה את שניהם מוחזרת
כ-``{"ok": false, "error": "outline_and_lines"}``, ולא מתעלמת בשקט מאחד
Expand Down Expand Up @@ -784,6 +864,11 @@ RST
הסעיף שמגדיר אותה, ומפה שאומרת באילו שורות הסעיף יושב מאפשרת להמשיך ישר
ל-``lines=``.

**ומה שמעגן את הכללים שלמטה:** זיהוי הכותרות מיושר לפארסר של docutils
0.23, ונבדק מול כל קובצי ה-RST של התיעוד הזה — אותן רמות ואותה ספירה בכל
קובץ — ומול טבלאות של צורות כותרת סינתטיות. זה נאמר כאן כי הכללים נראים
כמו החלטות שלנו, והם אינם: הם מה ש-Sphinx יעשה עם אותו קובץ.

.. important::

**אין משמעות מובנית לתו הפיסוק.** ``===`` אינו "רמה 1". ההיררכיה נקבעת
Expand Down Expand Up @@ -844,6 +929,15 @@ RST
``start`` מצביע לשורה בקובץ המקור, ושם מנורמל לא היה תואם לשורה שאליה
הוא מצביע.

.. warning::

**מחלקה אחת מוצהרת שאינה ממודלת: בלוק literal מצוטט.** תקן RST מתיר
בלוק שאינו מוזח ששורותיו נפתחות בתו פיסוק, ו-Sphinx קורא את כולו
כטקסט מילולי. הסורק כאן אינו מזהה את הצורה הזאת, ולכן שורת פיסוק
בתוכה עלולה להיראות לו כקו של כותרת — כלומר שורה במפה שאינה כותרת
בעמוד המרונדר. בקובצי התיעוד האלה אין לצורה אף מופע, והיא כתובה כאן
כדי שלא תיראה כבאג במי שייפגש בה בריפו אחר.

**וקובץ RST בלי אף כותרת אינו כשל** — הוא מחזיר מפה ריקה עם ``status:
outline``, כמו קובץ CSS בלי כללים. ``no_outline`` שמור לסיומת שאין לה
סורק ולתקרת הסימבולים.
Expand Down Expand Up @@ -888,6 +982,26 @@ outline``, כמו קובץ CSS בלי כללים. ``no_outline`` שמור לסי
המקור, ולא גרסה שהשתנתה בשקט. קיצוץ ה-``\r`` היה שובר את שתי התכונות
האלה, **וגם** מחזיר ספירה שסותרת את ``file.lines`` שיושב באותה תשובה.

.. note::

**``file.encoding`` אומר פחות ממה שהוא נראה, וכדאי לדעת מה.** הפענוח
מנסה קודם חתימת BOM ואחר כך רשימה, ב-``services/git_mirror_service.py``:

- ``utf-8`` — הקובץ פוענח כ-UTF-8. זה הרוב המוחלט.
- ``utf-8-sig`` — היה **BOM** בתחילת הקובץ. הוא **מוסר מ-``content``**,
וזה החריג היחיד ל"בית-בית כפי שהוא בקובץ" שלמעלה. ``size`` מגיע
מ-git וסופר את הבלוב, כלומר **כולל** את שלושת הבתים שלא תקבלו,
ו-``lines`` נספר על הטקסט אחרי ההסרה.
- ``latin-1`` — **נפילה-לאחור ולא זיהוי.** ``latin-1`` מפענח כל רצף
בתים ואינו נכשל לעולם, ולכן הוא תופס כל מה ש-UTF-8 דחה. נמדד: קובץ
עברי שנכתב ב-``cp1255`` חוזר עם התווית ``latin-1`` ועם תוכן משובש —
``שלום`` הופך ל-``ùìåí``. התווית נראית בטוחה, והתוכן אינו.

**המשמעות המעשית:** ``utf-8`` ו-``utf-8-sig`` הם זיהוי, ו-``latin-1``
הוא "שום דבר אחר לא עבד". קובץ שחוזר עם התווית הזאת ראוי לחשד לפני
שנשענים על תוכנו — ובעברית זה המקרה השכיח, כי ``cp1255`` אינו נגיש
ברשימה אחרי ``latin-1``.

.. list-table::
:header-rows: 1
:widths: 34 66
Expand Down Expand Up @@ -1264,6 +1378,12 @@ outline``, כמו קובץ CSS בלי כללים. ``no_outline`` שמור לסי
- שאילתה ארוכה משם פתק אפשרי לא תתפוס דבר. קצרו אותה
* - ``sync_in_progress``
- רענון רץ ברגע זה — נסו שוב לפי ``retry_after``
* - אין שום לוג מהשרת, גם כשהכול תקין
- תהליך ה-MCP אינו מגדיר לוגים: הלוגר השורשי ב-``WARNING`` ובאפס
handlers, ולכן רשומות ``debug``/``info`` שכתובות בקוד אינן נכתבות
לשום מקום. **זה אינו סימן שהכלי לא נקרא** — אבחון דרך הלוגים של
השרת אינו זמין היום, ומדידת השימוש עוברת ב-PostHog. חיבור הלוגים
מתועד באישו #3375
* - ``repo_or_ref_not_found``
- ה-mirror עוד לא שוכפל מקומית (המתינו למעבר autosync) או שאין דיסק/``REPO_MIRROR_PATH``

Expand Down
2 changes: 2 additions & 0 deletions docs/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ What's New

2026-09-14
----------
- **fix: אאוטליין של ``.rst`` פרסר את הקובץ כולו לפני שהתקרה נגעה בו.** התקרה על מספר הסימבולים יושבת בסורקים בכוונה — סינון של פלט אינו חסימה של עבודה — אבל בסורק ה-RST היא נגעה רק ברשימה שנבנתה **אחרי** ש-``parse_document`` סיים. כלומר קובץ שנכנס בנוחות לתקרת ה-10MB של קריאת הטווח יכול לקנות עבודה גדולה: נמדד על 10MB של כותרת בת תו אחד בכל שתי שורות — 2.6 מיליון סקשנים — **18.5 שניות ו-1,132MB**, מול **0.30 שניות ו-83MB** אחרי התיקון. הזיכרון הוא מה שמצדיק את זה: בקשה איטית היא בקשה איטית, אבל ג'יגה-בייט בתהליך משותף מפיל שירות. ``parse_document`` מקבל עכשיו ``max_sections`` שברירת המחדל שלו ללא תקרה, ולכן ``docs_get_section`` — הצרכן השני של הפארסר — זהה בית-בית, והתשובה על קובץ שחוצה את התקרה נשארה בדיוק אותה תשובה: ``no_outline`` עם ``too_many_symbols``, בלי רשימה חלקית. **ומה שלא נחסם, במפורש:** התקרה מגבילה סקשנים ולא שורות, ולכן 10MB של שורות ריקות הוא אפס סקשנים ו-6.3 שניות — זהה עם התקרה ובלעדיה.
- **docs: הקבועים והתקרות של שרת ה-MCP במקום אחד, ואוצר המילים של התשובה.** נוסף סעיף :ref:`mcp-limits` עם כל תקרה שנאכפת בקוד — כולל תקרות העימוד, שלא היו מתועדות כלל — ועם הכלל שערך מעל התקרה **נצמד בשקט** ואינו נדחה, כלומר ``per_page=1000`` באאוטליין מקבל 500 ואין שדה שמצהיר על זה. ובנוסף: הרשימה הסגורה של ערכי ``status`` **בתשובת כלי** (תשובת HTTP של השרת אינה תשובת כלי — ``401`` נושא ``error`` בלי ``ok``), ההסבר למה ל-``error`` אין רשימה כזאת, המחלקה היחידה שסורק ה-RST אינו ממודל (בלוק literal מצוטט), ושורה בפתרון תקלות על כך שתהליך ה-MCP אינו מגדיר לוגים — ולכן היעדר לוג אינו סימן שהכלי לא נקרא. **ובנוסף הוסבר מה ``file.encoding`` באמת אומר:** ``utf-8`` ו-``utf-8-sig`` הם זיהוי, ו-``latin-1`` הוא נפילה-לאחור שאינה נכשלת לעולם — נמדד שקובץ עברי ב-``cp1255`` חוזר עם התווית ``latin-1`` ועם תוכן משובש. וש-BOM **מוסר** מ-``content`` בזמן ש-``size`` סופר אותו, כלומר זה החריג היחיד לכלל שהטקסט חוזר בית-בית.
- **fix: שעון חוזר של קורא חיצוני הפך לחד-פעמי בשקט אחרי הערכת regex.** ``ConditionOperators.regex`` דורס את ``SIGALRM`` לשנייה אחת כדי לעצור ביטוי שמתפוצץ, ומשחזר אחר כך את השעון של מי שקרא לו. ``signal.getitimer`` מחזיר **זוג** — מה שנשאר עד הירייה הבאה, ומרווח החזרה — והקוד שמר את האיבר הראשון בלבד והשחזור העביר ארגומנט אחד. כלומר שעון **חוזר** שוחזר כחד-פעמי: הירייה הבאה כן מגיעה, וכל אלה שאחריה אינן, בלי שום שגיאה. נמדד: על שעון של 30 שניות עם חזרה כל 5, הקוד שלפני התיקון החזיר מרווח ``0.0`` אחרי הקריאה והמתוקן מחזיר ``5.0``. **בריפו הזה אין מסלול הגעה ידוע** — ``pytest-timeout`` מצמיד שעון חד-פעמי — ולכן זה פגם נכונות בקוד ייצור ולא תקלה שנצפתה. שלושת הטסטים שכיסו את השחזור הדליקו שעון חד-פעמי בלבד, ולכן אף אחד מהם לא היה יכול לראות את זה, והעזר שבטסטים נשא בעצמו את אותו פגם — גם הוא שמר איבר אחד.
- **fix: שתי בדיקות שלא היו מסוגלות לתפוס את מה שהן שומרות עליו.** העד של סורק ה-escape-ים שחזר את הסריקה בתוך עצמו במקום לקרוא למימוש, ולכן מי שישבור את המימוש היה מקבל אפס ממצאים ושתי הבדיקות היו ממשיכות לעבור; הוא עובר עכשיו דרך ``_invalid_escapes`` על קובץ שנשתל ב-``tmp_path``. **ושורת הסיכום שאומרת "כל בדיקות הדפדפן דולגו והכיסוי אפס" לא נכתבה דווקא בתצורה שה-CI מריץ.** הסיבה נרשמה במשתנה ברמת המודול שהפיקסצ'ר ממלא, וה-CI מריץ ``pytest -n auto --dist=loadscope`` — כלומר הפיקסצ'ר רץ ב-worker ו-``pytest_terminal_summary`` רץ ב-controller, ושם המשתנה ריק לעולם. נמדד על קובץ דפדפן אחד בלי דפדפן: בלי ``-n`` השורה נכתבת, ועם ``-n 2 --dist=loadscope`` הריצה מסתיימת ב-``4 skipped`` בלי שום אזכור של הסוויטה. **הפתרון הוא מקור אחד במקום שניים:** הסיבה נקראת מדיווחי הדילוג של pytest, שכן עוברים ל-controller בשתי התצורות — ``when == "collect"`` כשהחבילה חסרה, ו-``when == "setup"`` כשרק הדפדפן חסר, וזו הצורה שה-CI מגיע אליה כי ``requirements/base.txt`` מצמיד את ``playwright`` ואין באף workflow שלב שמתקין דפדפן. אומת בהרצה על ``pytest 8.4.2`` עם ``pytest-xdist 3.8.0`` — הגרסאות שה-CI מתקין, ולא אלה שמותקנות בקונטיינר הפיתוח. **ושתי הבדיקות ההתנהגותיות מריצות עכשיו את תת-התהליך בשתי התצורות**, כי שתיהן הריצו אותו בלי ``-n`` ולכן אף אחת מהן לא הייתה מסוגלת לראות את זה.

Expand Down
Loading
Loading