מה קורה
כשסוכן שולח לפרמטר מסוג str | None מחרוזת שנראית כמו JSON, ה-SDK של MCP ממיר אותה לפני שהכלי רואה אותה. המחרוזת "null" הופכת ל-None, כלומר ל"לא נשלח", ומערך או אובייקט JSON הופכים לרשימה או למילון ונדחים בוולידציה כ"לא מחרוזת".
נמדד על c11d428 (הראש של #3470), עם mcp 1.28.1 ו-pydantic 2.12.3, דרך mcp.call_tool מעל ProductionBackend ודמה של המסד:
| קריאה |
מה חוזר |
codekeeper_get_file(file_name=..., query="null") |
הקובץ המלא — אותה תשובה בדיוק כמו קריאה בלי query |
codekeeper_get_file(file_name=..., section="null") |
הקובץ המלא |
codekeeper_docs_get_section(..., section="null") |
עץ הכותרות (mode: toc), כאילו לא ביקשו סעיף |
codekeeper_get_file(file_name="null") |
{"found": false} — קובץ ששמו null אינו נקרא לפי שם |
codekeeper_get_file(file_name=..., query="[1, 2]") |
ToolError: validation error for get_fileArguments |
codekeeper_get_file(file_name=..., query='{"a": 1}') |
ToolError: validation error for get_fileArguments |
query="true" / query="123" |
עובדים כרגיל, חיפוש מילולי |
כלומר חיפוש מילולי של null, של מערך JSON או של אובייקט JSON בקובץ JSON שמור אינו אפשרי היום, ושניים מהם מחזירים תשובה אחרת לגמרי בלי שום סימן.
למה
FuncMetadata.pre_parse_json ב-mcp/server/fastmcp/utilities/func_metadata.py (שורות 134–172 ב-1.28.1, נקרא מהחבילה המותקנת): לכל ארגומנט שה-annotation שלו אינו בדיוק str, ושערכו מחרוזת, הוא מריץ json.loads. אם התוצאה היא str, int או float — המחרוזת נשארת כמו שהיא (ולכן "true" ו-"123" לא נפגעים: bool הוא תת-מחלקה של int). אחרת — None, רשימה או מילון — התוצאה מחליפה את המחרוזת. ה-docstring שם מסביר שזה נועד ל-Claude Desktop, ששולח רשימות ומילונים כמחרוזות JSON. אצלנו זה פוגע בדיוק בפרמטרים שאמורים לקבל טקסט חופשי.
ההיקף
28 פרמטרים מסוג "מחרוזת או null" ב-13 כלים (נספר מ-arg_model של הכלים הרשומים):
codekeeper_add_to_collection.folder, .note · codekeeper_create_board_note.color, .mode, .title · codekeeper_create_note.anchor_text, .color · codekeeper_create_repo_note.color, .mode, .title · codekeeper_docs_get_section.ref, .repo, .section · codekeeper_get_collection_items.folder · codekeeper_get_file.file_id, .file_name, .query, .section · codekeeper_get_repo_file.ref, .symbol · codekeeper_list_repo_tree.path, .ref · codekeeper_save_file.language · codekeeper_search_code.language · codekeeper_search_repo.file_pattern · codekeeper_update_note.anchor_text, .color, .content
לא מדדתי את ההשפעה בכלי הכתיבה (למשל codekeeper_update_note(content="null")), כי זה דורש דמה של שכבת הפתקים. ההמרה עצמה קורית בכולם, לפני הכלי, ולכן בכל אחד מהם הערך שמגיע לכלי הוא None, רשימה או מילון.
כיוון לתיקון שורש (לא מומש)
ב-AdminAwareFastMCP (mcp_server/server.py), שכבר עוטפת את add_tool: לדלג על ה-pre-parse לפרמטרים שה-annotation שלהם כולל str, כך שמחרוזת תגיע לכלי כמחרוזת. זו אותה מחלקה של "המרה בגבול לפני הוולידציה" כמו StrictInt/StrictLines (#3316) ו-StrictBool (#3470). ולצד התיקון — טסט שעובר על כל הכלים הרשומים ושולח "null" ו-"[1]" לכל פרמטר כזה, כדי שפרמטר חדש לא יחזור על זה.
קישורים
מה קורה
כשסוכן שולח לפרמטר מסוג
str | Noneמחרוזת שנראית כמו JSON, ה-SDK של MCP ממיר אותה לפני שהכלי רואה אותה. המחרוזת"null"הופכת ל-None, כלומר ל"לא נשלח", ומערך או אובייקט JSON הופכים לרשימה או למילון ונדחים בוולידציה כ"לא מחרוזת".נמדד על
c11d428(הראש של #3470), עםmcp1.28.1 ו-pydantic2.12.3, דרךmcp.call_toolמעלProductionBackendודמה של המסד:codekeeper_get_file(file_name=..., query="null")querycodekeeper_get_file(file_name=..., section="null")codekeeper_docs_get_section(..., section="null")mode: toc), כאילו לא ביקשו סעיףcodekeeper_get_file(file_name="null"){"found": false}— קובץ ששמוnullאינו נקרא לפי שםcodekeeper_get_file(file_name=..., query="[1, 2]")ToolError:validation error for get_fileArgumentscodekeeper_get_file(file_name=..., query='{"a": 1}')ToolError:validation error for get_fileArgumentsquery="true"/query="123"כלומר חיפוש מילולי של
null, של מערך JSON או של אובייקט JSON בקובץ JSON שמור אינו אפשרי היום, ושניים מהם מחזירים תשובה אחרת לגמרי בלי שום סימן.למה
FuncMetadata.pre_parse_jsonב-mcp/server/fastmcp/utilities/func_metadata.py(שורות 134–172 ב-1.28.1, נקרא מהחבילה המותקנת): לכל ארגומנט שה-annotation שלו אינו בדיוקstr, ושערכו מחרוזת, הוא מריץjson.loads. אם התוצאה היאstr,intאוfloat— המחרוזת נשארת כמו שהיא (ולכן"true"ו-"123"לא נפגעים:boolהוא תת-מחלקה שלint). אחרת —None, רשימה או מילון — התוצאה מחליפה את המחרוזת. ה-docstring שם מסביר שזה נועד ל-Claude Desktop, ששולח רשימות ומילונים כמחרוזות JSON. אצלנו זה פוגע בדיוק בפרמטרים שאמורים לקבל טקסט חופשי.ההיקף
28 פרמטרים מסוג "מחרוזת או
null" ב-13 כלים (נספר מ-arg_modelשל הכלים הרשומים):codekeeper_add_to_collection.folder,.note·codekeeper_create_board_note.color,.mode,.title·codekeeper_create_note.anchor_text,.color·codekeeper_create_repo_note.color,.mode,.title·codekeeper_docs_get_section.ref,.repo,.section·codekeeper_get_collection_items.folder·codekeeper_get_file.file_id,.file_name,.query,.section·codekeeper_get_repo_file.ref,.symbol·codekeeper_list_repo_tree.path,.ref·codekeeper_save_file.language·codekeeper_search_code.language·codekeeper_search_repo.file_pattern·codekeeper_update_note.anchor_text,.color,.contentלא מדדתי את ההשפעה בכלי הכתיבה (למשל
codekeeper_update_note(content="null")), כי זה דורש דמה של שכבת הפתקים. ההמרה עצמה קורית בכולם, לפני הכלי, ולכן בכל אחד מהם הערך שמגיע לכלי הואNone, רשימה או מילון.כיוון לתיקון שורש (לא מומש)
ב-
AdminAwareFastMCP(mcp_server/server.py), שכבר עוטפת אתadd_tool: לדלג על ה-pre-parse לפרמטרים שה-annotation שלהם כוללstr, כך שמחרוזת תגיע לכלי כמחרוזת. זו אותה מחלקה של "המרה בגבול לפני הוולידציה" כמוStrictInt/StrictLines(#3316) ו-StrictBool(#3470). ולצד התיקון — טסט שעובר על כל הכלים הרשומים ושולח"null"ו-"[1]"לכל פרמטר כזה, כדי שפרמטר חדש לא יחזור על זה.קישורים
"null"בטבלת מצבי הקצה):docs/mcp-server.rst, הסעיף "קריאה לפי סעיף בקובץ Markdown שמור" — נוסף ב-feat(mcp): קריאה לפי סעיף בקובץ Markdown שמור — toc ו-section ב-codekeeper_get_file (PR ג) #3470.הצעת-דפוס-המרה-בגבול-לפני-הוולידציה.md.