מעבר לכלים המובנים, אתר יכול לפרסם כלי MCP משלו: שיבוץ כתבה בדף הבית, חישוב מחיר, שליחת לקוח למערכת חיצונית. הכלי נרשם ב-AGENT_TOOLS::add_tool ונגיש גם ללקוחות MCP וגם לסוכני AI, כולל dry-run, ledger ותור אישורים. בדף הזה תכתבו כלי קריאה וכלי כתיבה מלאים ותבדקו אותם.
AGENT_TOOLS::boot() עושה require_once לכל קובץ application/admin/includes/*.php (נתיב יחסי לשורש האתר), כל קובץ בתוך try/catch נפרד. שגיאה בקובץ אחד נרשמת ב-error_log ולא מפילה את האחרים. אין קונבנציה של application/ai_tools/ לכלי MCP: התיקייה הזו שייכת למנגנון ה-cron הישן.החוזה של add_tool#
חתימה: AGENT_TOOLS::add_tool(array $def). הפונקציה זורקת Exception על הגדרה לא חוקית. ב-boot() החריגה נתפסת, והכלי פשוט לא מופיע.
| מפתח | חובה | כללים |
|---|---|---|
name | כן | ^[a-z0-9_]{2,40}$, ולא אחד מהשמות השמורים ב-AGENT_TOOLS::RESERVED |
label | לא | שם בעברית ל-UI ולציר הזמן של הריצה. ברירת מחדל: ה-name |
description | כן | הטקסט שהמודל קורא כדי להחליט מתי להשתמש בכלי. נחתך ל-1000 תווים בפרסום |
input | לא | מפת properties שטוחה של JSON Schema. מילות המפתח format, examples, $schema, contentEncoding, contentMediaType מוסרות אוטומטית |
required | לא | רשימת שמות שדות |
write | לא | true: ב-dry-run הכלי מדומה, ב-approve_writes הוא נכנס לתור, והקריאה נרשמת ב-ledger |
_meta | לא | מפה עם מפתחות מחרוזת בלבד, מתפרסמת כפי שהיא לצד inputSchema. המפתח openai/fileParams חייב להצביע על שמות מתוך input |
handler | כן | callable בחתימה function(array $args, array $ctx) |
ה-handler מקבל $ctx עם admin_id, agent_id, run_id ו-tools (מופע admin_mcp_tools). ערך ההחזרה:
- מערך הופך ל-
dataשל התשובה. - ערך סקלרי הופך ל-
["result" => ...]. - מערך עם המפתח
__errorאוthrowהם כישלון. ההודעה חוזרת ללקוח.
השם מתפרסם ב-tools/list כ-<name>, וברשימת הכלים של סוכן הוא נרשם כ-cms:<name>.
RESERVED מכיל רק 11 שמות. כלי ליבה כמו form_action, db_schema, update_playbook, generate_image, import_media, delete_media, search_media, כלי seo_* וקידומות של מוצרים (kama_, seok_ ועוד) לא חסומים בקוד, אבל כלי שיצטרף איתם לאותו שם יתנגש. בחרו שם עם קידומת של האתר, למשל hp_ או shop_.דוגמה מלאה#
הקובץ application/admin/includes/mcp_tools.php. הטבלה hp_slots היא טבלת דוגמה של האתר (קוד סלוט, כותרת, מזהה כתבה), לא טבלה של הליבה:
<?php
/*
* כלי MCP של האתר. הקובץ נטען גם בבקשות /system/mcp ו-/system/agent_mcp
* (לא רק בעמודי אדמין), לכן: אין פלט, אין תלות ב-$_GET, אין מחלקות FormInput_*.
*/
if (!class_exists("AGENT_TOOLS")) return;
// 1. כלי קריאה: הסלוטים של דף הבית
AGENT_TOOLS::add_tool([
"name" => "hp_get",
"label" => "סלוטים בדף הבית",
"description" => "List the homepage slots (code, title, current article id). Call before hp_place.",
"input" => (object)[],
"handler" => function (array $args, array $ctx) {
return ["slots" => DB::get_all("SELECT code, title, article_id FROM CRM_hp_slots ORDER BY ord")];
},
]);
// 2. כלי כתיבה: שיבוץ כתבה בסלוט, עם בדיקת הרשאה ואימות קלט
AGENT_TOOLS::add_tool([
"name" => "hp_place",
"label" => "שיבוץ בדף הבית",
"description" => "Place an article in a homepage slot. Use hp_get first to learn the slot codes. Fails if the article is not published.",
"input" => [
"item_id" => ["type" => "integer", "description" => "the article id"],
"position" => ["type" => "string", "description" => "slot code from hp_get"],
],
"required" => ["item_id", "position"],
"write" => true, // dry-run: מדומה. approve_writes: נכנס לתור
"handler" => function (array $args, array $ctx) {
// 1. הרשאה: הכלי רץ בשם $ctx["admin_id"]. ADMIN::has_perms קורא את ה-session
// הנוכחי ולכן לא מתאים כאן, בודקים את הקבוצה של המנהל ישירות.
$admin = DB::get_row("SELECT group_id FROM CRM_adminPanel_admins WHERE id = " . (int)$ctx["admin_id"]);
$group = $admin ? DB::get_row("SELECT perms FROM CRM_adminPanel_admins_groups WHERE id = " . (int)$admin["group_id"]) : false;
$perms = $group ? (string)$group["perms"] : "";
$panel = DB::get_val("adminPanel_panels", ["panel_name" => "homepage"]);
$ids = array_column((array)json_decode($perms, true), "id");
$allowed = ($perms === "-1") || ($panel && in_array((int)$panel["id"], array_map("intval", $ids), true));
if (!$allowed) return ["__error" => "the admin running this tool has no permission on the homepage panel"];
// 2. אימות קלט
$id = (int)($args["item_id"] ?? 0);
$pos = preg_replace('/[^a-z0-9_]/', '', strtolower((string)($args["position"] ?? "")));
if ($id <= 0 || $pos === "") return ["__error" => "item_id and position are required"];
$article = DB::get_val("articles", $id);
if (!$article || (int)$article["is_active"] !== 1) return ["__error" => "article #$id is not published"];
$slot = DB::get_val("hp_slots", ["code" => $pos]);
if (!$slot) return ["__error" => "unknown slot '$pos' (see hp_get)"];
// 3. כתיבה: update לפי מזהה השורה
$up = DB::update("hp_slots");
$up->set_var("article_id", $id);
$up->set_var("placed_by", (int)$ctx["admin_id"]);
$up->set("placed_at", "FUNC(NOW())");
$up->update((int)$slot["id"]);
// אפשר להפעיל כלי ליבה מתוך הכלי שלכם (אותו הקשר, אותו dry-run ו-ledger):
// $ctx["tools"]->call("notify_admin", ["message" => "שובצה כתבה $id בסלוט $pos"]);
return ["placed" => true, "slot" => $pos, "article_id" => $id];
},
]);
כמה נקודות בדוגמה:
- ה-handler מקבל קלט מהמודל, ולכן מנקים ובודקים אותו כמו כל קלט חיצוני. בשאילתת SQL גולמית משתמשים ב-
CRM_מלא וב-DB::escape. בבונה (DB::get_val,DB::update) כותבים שם טבלה בליCRM_. set_varו-setבונים את העדכון, ו-update($id)מריץ אותו ומחזיר bool. הערך"FUNC(NOW())"ב-setנשלח ל-SQL כפונקציה ולא כמחרוזת.- אל תשתמשו ב-
DB::insert: הוא לא קיים. הוספה נעשית ב-DB::update("table")->set_var(...)->insert().
כלי מובנה כמו save_record עובר דרך הפאנל, והפאנל אוכף הרשאות. ה-handler שלכם ניגש ל-DB ישירות, ולכן אתם אחראים לבדוק הרשאות. אם הכלי משפיע על מטמון, פתחו אותו מחדש בקוד שלכם (ראו cache_engine API).
הפעלת כלי ליבה מתוך כלי#
$ctx["tools"] הוא מופע admin_mcp_tools באותו הקשר (אותו מנהל, ריצה, dry-run). קריאה ל-->call("save_record", [...]) עוברת דרך הפאנל והרשאותיו ומחזירה ["ok"=>..., "data"=>...]. בצורה כזאת כלי אחד יכול להרכיב פעולות מובנות בלי לשכפל ולידציה.
בדיקה#
- שלחו
tools/listובדקו ש-hp_getו-hp_placeמופיעים (ראו MCP ב-WIZZO CMS). - הפעילו את הכלי עם
__dry_run: trueובדקו שכלי עםwrite => trueלא משנה נתונים. - הוסיפו לסוכן את
cms:hp_placeב-סוכני AI.
{
"jsonrpc": "2.0", "id": 4, "method": "tools/call",
"params": { "name": "hp_place", "arguments": { "item_id": 1234, "position": "top", "__dry_run": true } }
}
שם לא חוקי, תיאור ריק או handler שאינו callable זורקים חריגה שנתפסת ב-boot(). בדקו את error_log של האתר: השורה מתחילה ב-AGENT_TOOLS boot:.