feat(mcp): כל כלי קריאה מצהיר על תקרת התשובה שלו ועומד בה — מדידה אחת, ורשת אחרונה (#3460, #3474, #3481)
תבנית Pull Request
✨ תיאור קצר
- Claude Code שומר לקובץ כל תשובת כלי שעוברת את הסף שלו (25,000 טוקנים כברירת מחדל,
MAX_MCP_OUTPUT_TOKENS), חוץ מכלי שמצהיר ב-tools/listעל_meta["anthropic/maxResultSizeChars"]: לכלי כזה הערך המוצהר, בתווים, מחליף את סף הטוקנים לתוכן טקסט (code.claude.com/docs/en/mcp.md, "MCP output limits and warnings", נקרא שוב ב-2026-09-29). עד היום רקcodekeeper_read_batchהצהיר, ולכן קובץ עברי של 52,158 תווים (13 שורות) הגיע לסוכן כקובץ ולא כטקסט. - עכשיו כל כלי קריאה מצהיר על התקרה שלו, ועומד בה בעצמו: המדידה היא של מה שבאמת נשלח. מאחורי הכלים יושבת רשת אחרונה שמסרבת לתשובה שעוברת את מה שהוצהר.
- בדרך נסגרו שני באגים באותו שורש: המדידה של
query(#3474), וחיתוך באמצע עמוד ב-list_repo_tree(#3481). - עדכון — תיקוני הריוויו של Han (קומיט
ef6a27d): מטא-דאטה של קובץ שמור כבר לא דוחקת את התוכן, "טווח או קריאה מלאה" נקבע לפי הבקשה, וכל ההתאמה יושבת במודול אחד. הפירוט בסעיף 7.
Closes #3460
Closes #3474
Closes #3481
📦 שינויים עיקריים
- קוד (Backend)
- בוט טלגרם
- מסד נתונים/מיגרציות
- תיעוד (docs/)
- DevOps/CI/CD
פירוט נקודות:
1. מדידה אחת, במודול אחד — mcp_server/answer_size.py (חדש).
- מודול בלי תלויות פנימיות. הוא מחזיק את התקציב (
OUTPUT_BYTE_BUDGET), את המדידה כפי שה-SDK שולח (wire_json,list_item_cost), את אוצר המילים (ANSWER_TOO_LARGE,BYTE_BUDGET_REASON) ואת ההצהרה ללקוח (declared_size_meta). - בזכותו נשבר המעגל handlers ↔ repo_handlers, ש-
QUERY_OUTPUT_BYTE_BUDGETהיה עותק שלו בגללו. העותק נמחק.
2. כל כלי עומד בתקרה בעצמו.
| כלי ומצב | על main (נמדד דרך call_tool) | עכשיו |
|---|---|---|
get_file, קריאה מלאה של 300K תווים עבריים | 547,927 בתים | answer_too_large (548 בתים), עם הפניה ל-lines/query (ב-Markdown: toc/section) |
get_file, lines=[1, ∞] | 548,037 | נגמר על גבול שורה (255,900), range.truncation_reason: "byte_budget", וממשיכים מ-end + 1 |
get_file, query לפי המתכון של #3474 | 259,168 | 249,041, truncation_reason: "byte_budget" |
get_file, toc עם תיאור של 200K תווים | הסירוב עצמו 400,523 | אחרי הריוויו: מפת הכותרות מגיעה בהצלחה, התיאור ירד שלם, ובמקומו file.description_bytes ורמז ל-codekeeper_update_file_description |
get_repo_file, docs/mcp-server.rst מלא | 299,038 | answer_too_large (771), עם lines/outline ו-read_with |
get_repo_file, webapp/app.py עם lines=[1, ∞] | 932,955 | 255,991, נגמר על גבול שורה |
outline, CSS עם per_page=500 | נמדדו 247,684 (חלון דחוס), נשלחו 261,992 | page_too_large עם bytes=261,992 |
search_repo, שורות מלאות מירכאות | נמדדו 248,234 (str()), נשלחו 374,931 | 250,304 |
list_repo_tree, עמוד שלא נכנס | חיתוך באמצע העמוד, והמשכו אבד (#3481) | page_too_large על העמוד כולו, ואחרי הריוויו גם עם hint |
list_board_notes, 200 פתקים × 20,000 תווים | 7,173,206 | answer_too_large (380) עם הפניה ל-include_content=false. הקריאה מהמסד נעצרת מוקדם |
3. ההצהרה היא חלק מהרישום.
- שמונה כלים מצהירים:
get_file,get_repo_file,search_repo,list_repo_tree,list_notes,list_board_notes,docs_get_sectionו-read_batch. - כולם מצהירים
meta=answer_size.declared_size_meta(), עלOUTPUT_BYTE_BUDGET. בתים הם חסם עליון על התווים שהלקוח סופר. AdminAwareFastMCP.add_toolמסרב לערך שהלקוח לא יכבד: לאint,bool, אפס או פחות, או מעל 500,000.list_repo_notesאינו מצהיר, בכוונה. החשבון: 20 × 20,000 = 400,000 תווים, כלומר לפחות 400,000 בתים, ואין לו מצב רזה.
4. רשת אחרונה ב-AdminAwareFastMCP.call_tool.
- רצה בשני הענפים, וקוראת את התקרה מ-
Tool.meta. - מודדת כל בלוק וגם
structuredContent. צורה שאי אפשר למדוד מסורבת (fail-closed). - כתוב גם בקוד, ליד הרשת: תפיסה שלה היא באג בכלי, לא "המערכת עבדה".
- כל תפיסה נרשמת כ-WARNING עם הסמן
answer_size_net, שם הכלי והמספרים בלבד.
5. תיקונים קטנים באותו שורש.
- סירוב על קובץ שמור מוותר על תיאור ענק שלם, ובמקומו
file.description_bytes. זה הצד של הקריאה ב-#3489. השורש בכותבים נשאר פתוח שם. - ב-
search_repo, חיתוך של התקציב גובר עלmax_resultsשהמנוע דיווח.
6. תיעוד.
docs/mcp-server.rst: סעיף חדש,mcp-answer-size. עודכנו גם טבלת הקבועים, הטווחים,query, המפה, העץ, הבאץ', הפתקים ופתרון התקלות.docs/whats-new.rst,mcp_server/README.md.- תיאורי הכלים: משפט אחד משותף ל-
get_file/get_repo_file, והשורה על רשימות הפתקים.
7. תיקוני הריוויו של Han (קומיט ef6a27d).
- WARN-001 — מטא-דאטה לא דוחקת את התוכן. קודם, קובץ שמור עם תיאור ענק קיבל סירוב או טווח מקוצץ, גם כשהתוכן עצמו נכנס בנוחות. עכשיו ההכרעה ב-
backend._fit_with_file_meta, לפי שלושה כללים:- (א) תשובה שנכנסת — זהה בית-בית לקודם.
- (ב) תשובה שהייתה מסורבת או נחתכת, ובלי המטא-דאטה נכנסת שלמה — מוותרת קודם על התגיות ואחר כך על התיאור, עם סימן (
tags_truncated,description_bytes) ורמז ל-codekeeper_update_file_description. - (ג) כשהתוכן עצמו גדול — המטא-דאטה נשארת, אלא אם זה סירוב, או שהמטא-דאטה גדולה מכל שאר התשובה (
_metadata_outweighs). כך טווח של כמה שורות לא נחנק תחת תיאור של 200KB.
- WARN-002 — "טווח או קריאה מלאה" לפי הבקשה.
file_read_answerו-fit_file_answerמקבליםranged=lines is not None, ולא מסיקים את זה משדהrangeשמור במסמך. במראה זה לא היה באג (למסמך של מראה אין שדות משתמש), אבל הקוד עובר באותה דרך. - SUGG-001/002 — מודול
mcp_server/answer_fit.py(חדש).- בונה סירוב אחד (
too_large), סימון חיתוך אחד (cut(returned=, of=)),fit_readלשני כלי הקריאה,fit_refusalו-fit_lists. - הכינויים וייצואי-המשנה (
ANSWER_TOO_LARGEב-docs_handlers,UNREAD_BYTE_BUDGET/_wireב-read_batch,repo_handlers.OUTPUT_BYTE_BUDGETב-server ועוד) הוסרו. כל מודול מייבא ישירות, וטסט AST שומר על זה — כולל כינויים וייצוא-משנה עקיף.
- בונה סירוב אחד (
- SUGG-003 — סירוב
page_too_largeשל העץ נושאhint, והתיאור שלlist_repo_treeמזכיר אותו. - SUGG-004 — לוג INFO
answer_size_fitעל כלanswer_too_largeוחיתוךbyte_budget: שם הכלי, שמות הארגומנטים בלבד (בלי ערכים), והמספרים. הרשומות נאספות ב-ContextVar שנפתח ב-call_toolועובר לחוט העובד. - SUGG-005 — שלושה טסטים: תגיות ענקיות יחד עם תיאור ענק; טווח מהאמצע עם שני עותקי טקסט, על הגבול; רמז שמפנה לענף (
ref) הנכון. - שינוי התנהגות מכוון (JD-004), מתועד ב-whats-new: לקוח שאינו Claude Code, שהיה מקבל עד היום 400KB, מקבל עכשיו סירוב עם רמז.
🧪 בדיקות
- Unit
- Integration: session אמיתי של הפרוטוקול, עם PostHog דלוק ומראות git אמיתיות ב-
tmp_path. - הרצה חיה (סשן MCP אמיתי בזיכרון, מעל
ProductionBackendו-RepoBackendאמיתיים ומראות git אמיתיות), מפורטת למטה. - Manual: בדיקה אחרי דיפלוי — בוצעה אבל לא בדקה את ה-PR (רשימת כלים ישנה בצד הלקוח), מפורטת למטה. צריך לחזור עליה מסשן חדש.
קובץ טסטים חדש, tests/test_mcp_answer_size.py:
- הרשת: נבדקת עם תקרה קטנה שמוזרקת לכלי, כך שעלות הטסט לא קשורה לקבוע.
- שני הענפים: הרשת נבדקת גם בענף הרגיל וגם בענף של הבאץ'.
- כלי שנרשם בעקיפת
add_tool: גם הוא מוחזק לתקרה שלו. - כל בלוק נספר: וצורה שאי אפשר למדוד מסורבת.
- בדיקת הערך ברישום.
- שומר: דרך session אמיתי עם PostHog, ה-
_metaשהלקוח רואה שווה ל-Tool.meta, והכלים שמצהירים הם בדיוק השמונה. - הקלט הגרוע של כל אחד משמונת הכלים: כולל
docs_get_sectionוסבב מלא של טווח שנחתך, קריאת המשך וחיבור. התוצאה זהה לקובץ המקורי, בלי שורה כפולה ובלי חסרה, גם בקובץ גדול שבו התוכן יושב ב-codeוגם ב-content. - כל טסט של כלי מוודא שהרשת לא נדלקה, כלומר שהכלי התאים את התשובה בעצמו.
- המספרים בתיעוד: טסט מצמיד את המספרים בסעיף החדש לקבועים עצמם.
מול הקוד הישן: 33 מתוך 38 הטסטים החדשים נופלים על main, והנפילות הן על הגדלים עצמם (למשל 545437 <= 256000, 259187 <= 256000, 370076 <= 256000). חמישה עוברים שם, וזה צפוי:
- טסטים של אפס-דיף: תשובה שנכנסת לא משתנה.
docs_get_section, שכבר התאים את עצמו.- טסט מבני על העלה.
- הקבלה של ערך התקרה עצמו.
מוטציות (17), ב-worktree נפרד. כל אחת הפילה לפחות טסט אחד:
- הרשת: רשת רק בענף אחד; רשת שקוראת רישום שמתמלא ב-
add_tool;list_toolsשמזריק_meta; מדידה של הבלוק הראשון בלבד, או של טקסט בלבד; דילוג על בדיקת הערך. - טווחים:
endשמדווח את מה שביקשו. query: מדידה דחוסה, או בלי שמורה למעטפת.- באץ': פריט קובץ בלי התאמה, או תקציב על הקריאה המשותפת.
- פתקים: קריאה של כולם לפני המדידה, או חיתוך במקום סירוב.
- שאר הכלים: תיאור שנשאר בסירוב; מדידת חלון האאוטליין; עץ שלא נדחה; הצהרה שנמחקה מכלי; שורות חיפוש שנמדדות ב-
str();max_resultsשגובר על התקציב.
תיקוני הריוויו — טסטים:
- כל הטסטים החדשים נכתבו לפני התיקון ונפלו על הקוד שלפניו מהסיבה הנכונה.
- טסט הלוג רץ דרך
call_toolבתת-תהליך נקי, כשגוף הכלי רץ בחוט עובד — לא דרך caplog (הלקח של #3390/#3393). - טסט שקיבע את הבאג (סירוב של
tocבגלל תיאור) עודכן להתנהגות החדשה. החשבון של הסירוב עצמו נבדק עכשיו ישירות. - 11 מוטציות נוספות, כולן נהרגו: הקשר שלא עובר לחוט העובד; בלי שורת לוג;
_metadata_outweighsתמיד אמת / תמיד שקר; מסלול הצלחה ששומר מטא-דאטה; רמז חסר בהצלחה ובסירוב; הסתעפות לפיrangeשמור; רמז חסר בעץ;256_000מוקלד מחדש; חיתוך שלא נרשם.
הרצה חיה (אחרי תיקוני הריוויו): סשן MCP אמיתי, פעם על הענף לפני התיקונים ופעם אחריהם, והשוואת הבתים שהלקוח קיבל.
- הקובץ מ-#3460 גדל:
docs/source-projects/codebot-patterns.mdב-amir-bug-patterns הוא היום 133,844 בתים ב-1,045 שורות, ולא כ-52,000 תווים ב-13 שורות. לכן נבנה תחליף באותם ממדים (52,158 תווים עבריים, 13 שורות), והקובץ האמיתי רץ לצידו. - אפס-דיף: 316 מתוך 320 תשובות זהות בית-בית, כולל כל 300 תשובות הסריקה (כל קובץ
.mdב-amir-bug-patterns — כקובץ שמור, כ-toc, וכקובץ במראה). - ארבע התשובות ששונות הן בדיוק מקרי WARN-001: קובץ עם תיאור של כ-200KB. קריאה מלאה של התחליף: קודם סירוב, עכשיו התוכן המלא (92,854 בתים) בלי התיאור ועם רמז; אותו דבר בקובץ האמיתי (135,886 בתים);
tocו-section_not_foundירדו מ-244KB ל-177KB. - הלוג בפועל:
answer_size_fit: codekeeper_get_repo_file args=[lines, path, repo] byte_budget returned=1172 of=2500— שמות בלבד, בלי ערכים.
חבילת הטסטים המלאה: ירוקה, חוץ מכשלים שקיימים גם על main ואינם קשורים:
tests/test_infrastructure.py:isortו-autopep8לא מותקנים בסביבה.- טסטי דפדפן: נופלים רק בהרצה מקבילית, ועוברים כשמריצים אותם אחד-אחד (55 מתוך 55).
בדיקה אחרי דיפלוי (ידנית) — בוצעה, ולא בדקה את ה-PR:
- מה נבדק: קריאה מלאה דרך
codekeeper_get_repo_fileשלdocs/source-projects/codebot-patterns.mdב-amir-bug-patterns, מסשן Claude Code בענן, דרך ה-connector של claude.ai, מול השרת בפרודקשן. - השרת: שלח את התשובה שלמה ותקינה —
"ok": true, כל התוכן, 135,499 בתים, בתוךOUTPUT_BYTE_BUDGET. לא נחתך ולא סורב. - הלקוח: שמר את התשובה לקובץ, עם ההודעה
result (89,554 characters across 13 lines) exceeds maximum allowed tokens— כלומר הפעיל את סף הטוקנים (25,000 כברירת מחדל), ולא תקרה מוצהרת. - למה זה לא בודק את ה-PR: תיאור הכלים שהסשן החזיק ברגע הבדיקה הוא של
7be7f88, ה-main שלפני ה-PR: תיאורtocשלו מכיל את המשפט "a successful reply never cuts the file's metadata", שקיים ב-7be7f88ולא ב-383a05e, וב-7be7f88אף כלי קריאה לא מצהיר עלanthropic/maxResultSizeChars. רשימת הכלים של הסשן נטענה לפני הדיפלוי ולא התרעננה, ולכן מבחינת הלקוח הכלי לא הצהיר על כלום. - לפי התיעוד (
code.claude.com/docs/en/mcp.md, "MCP output limits and warnings"), כלי שמצהיר מקבל את התקרה המוצהרת לתוכן טקסט "regardless of whatMAX_MCP_OUTPUT_TOKENSis set to". הציפייה שהייתה כתובה כאן — שהקובץ יגיע להקשר — עדיין לא נבדקה, ולא הופרכה. - מה שהמדידה כן מראה: כלי שלא מצהיר נחסם בסף טוקנים, ו-89,554 תווים מול 135,499 בתים הם היחס של עברית — קובץ עברי ייחסם בסף הזה הרבה לפני קובץ אנגלי באותו גודל. זה בדיוק המצב ש-#3460 פתר עבור שמונת הכלים שמצהירים.
- הצעד הבא: לחזור על הבדיקה מסשן Claude Code חדש, שנפתח אחרי הדיפלוי.
🧪 בדיקות נדרשות ב‑PR
- 🔍 Code Quality & Security
- Unit Tests (3.11)
- Unit Tests (3.12)
📝 סוג שינוי
- feat: פיצ'ר חדש (ההצהרה והרשת)
- fix: תיקון באג (#3474, #3481, סירוב שעבר את התקציב, מטא-דאטה שדחקה תוכן)
- docs: שינוי תיעוד בלבד
- refactor:
answer_fit.py— מקום אחד להתאמה - perf: שיפור ביצועים
- chore/ci: תשתית/CI
- breaking change: שינוי צורת תשובה במקרים שעוברים את התקציב, בכוונה.
- קריאה מלאה גדולה מהתקציב: עכשיו
answer_too_largeבמקום התוכן. - טווח גדול מהתקציב: נגמר מוקדם, על גבול שורה.
- רשימת פתקים מלאה גדולה מהתקציב: עכשיו סירוב.
list_repo_treeמעל התקציב:page_too_large.- קובץ שמור שהמטא-דאטה שלו לא נכנסת לצד התוכן: התיאור/התגיות יורדים עם סימן ורמז.
- תשובה שנכנסת בתקציב לא משתנה בבית אחד.
- קריאה מלאה גדולה מהתקציב: עכשיו
✅ צ'קליסט
- הקוד עוקב אחרי הסגנון (flake8 נקי על הקבצים שנגעתי בהם, מלבד E302 שקיים על main ב-
backend.py) - בדיקות רצות ועוברות
- תיעוד עודכן (README/Docs)
- אם נוספו ג'ובים חדשים — לא רלוונטי
- אם נוספו/שונו משתני סביבה — לא נוספו
- אם נוספו/השתנו טוקנים — לא רלוונטי
- אין סודות/מפתחות בקוד. הלוגים (
answer_size_net,answer_size_fit) נושאים שם כלי, שמות ארגומנטים ומספרים בלבד. - אין מחיקות מסוכנות/פעולות על root
- הודעת הקומיט תואמת Conventional Commits
- CHANGELOG עודכן (
docs/whats-new.rst) - כל ה‑Required Checks לעיל ירוקים
- צילום/וידאו UI — לא רלוונטי
- עיינתי במסמכי אתר התיעוד:
AI-MAP.mddocs/mcp-server.rst(הכלים, הקבועים, מודל הריצה, קריאת טווח,query, מצב הסעיפים, מפת הסימבולים, העץ,read_batch,get_note, פתרון תקלות)docs/doc-authoring.rstdocs/versioning-stable-anchors.rstdocs/style-glossary.rstdocs/testing.rstdocs/error_codes.rst: מכסה קודיE_*של Sentry ולא סירובי MCP.events_catalog: שורת WARNING/INFO של stdlib אינה אירוע.- המשפט: "התיעוד הוא התמצאות, לא סמכות".
🧩 השפעות/סיכונים
- שינוי צורה רק מעל התקציב: תשובה שנכנסת זהה בית-בית (יש לזה טסטים והרצה חיה של 316 תשובות). שינוי הצורה שלמעלה חל רק על תשובות שעד היום נשמרו לקובץ אצל Claude Code, כלומר ממילא לא הגיעו לסוכן כטקסט.
- לקוח שאינו Claude Code: מקבל סירוב עם רמז במקום תשובה של מאות KB (מתועד ב-whats-new).
- ביצועים:
- קריאה מלאה מסדרת את התשובה פעם אחת כדי למדוד אותה. זה פרופורציונלי: המסמך כבר נטען ומחושב לו hash.
- טווח ורשימות נמדדים בהצטברות, בלי לבנות את הטקסט המלא.
- רשימת פתקים נעצרת במסד ברגע שהתקציב עבר.
- ניסיון ללא מטא-דאטה רץ רק כשהתשובה הראשונה סורבה או נחתכה.
- PostHog: האירוע
$mcp_tool_callנלכד מתחת לרשת, ולכן תפיסה של הרשת נראית רק בלוג (answer_size_net).
🔗 קישורים
- Issues קשורים: #3460, #3474, #3481, #3457 (סעיף 1 נסגר כאן; סעיפים 2–4 מוקפאים), #3489 (צד הקריאה בלבד)
- Docs Preview: ה-check של Read the Docs על ה-PR
- מסמכים/מפרטים רלוונטיים:
docs/mcp-server.rst→ "תקרת התשובה — מה כלי מצהיר, ומי אוכף" (mcp-answer-size) - Branch Protection & PR Rules
🧯 סיכון / החזרה לאחור (Rollback)
- revert של הקומיטים. אין מיגרציה ואין שינוי במסד.
- אחרי revert, הכלים חוזרים לשלוח תשובות גדולות, ו-Claude Code חוזר לשמור אותן לקובץ.
🤖 Generated with Claude Code