* Copyright (C) 2022 Alice Adminson * Copyright (C) 2024 MDW * Copyright (C) 2024 Frédéric France * Coryright (C) 2024 Alexandre Spangaro * Copyright (C) 2026 Nick Fragoulis * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY, without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file htdocs/ai/admin/configure_tools.php * \ingroup ai * \brief Admin page to configure which MCP tools are available per context. * * Two independent allow-lists are managed here: * AI_ASSISTANT_ALLOWED_TOOLS — tools visible to the private chat assistant * AI_MCP_SERVER_ALLOWED_TOOLS — tools visible to external MCP clients * * System tools (isSystem() = true) are always shown as locked-on and are never * written into the allow-list constants — McpHandler forces them through at runtime. * * Constant semantics (same for both constants): * '' → not yet configured → all non-system tools are allowed (default open) * 'NONE' → admin explicitly disabled every non-system tool * else → comma-separated list of allowed tool names (explicit allow-list) */ require '../../main.inc.php'; /** * @var Conf $conf * @var DoliDB $db * @var HookManager $hookmanager * @var Translate $langs * @var User $user */ require_once DOL_DOCUMENT_ROOT . '/core/lib/admin.lib.php'; require_once DOL_DOCUMENT_ROOT . '/core/class/html.form.class.php'; require_once DOL_DOCUMENT_ROOT . '/ai/lib/ai.lib.php'; require_once DOL_DOCUMENT_ROOT . '/ai/class/mcp.class.php'; require_once DOL_DOCUMENT_ROOT . '/core/lib/functions2.lib.php'; $langs->loadLangs(array('admin', 'other', 'main')); // Access control if (!$user->admin) { accessforbidden(); } if (!isModEnabled('ai')) { accessforbidden('Module AI not activated.'); } // Parameters $action = GETPOST('action', 'aZ09'); $toolcontext = GETPOST('toolcontext', 'alpha'); $toolname = GETPOST('toolname', 'alpha'); $mode = GETPOST('mode', 'alpha'); $backtopage = GETPOST('backtopage', 'alpha'); // Load unfiltered schema $mcpHandler = new McpHandler($db, $user, $conf, McpHandler::CTX_ASSISTANT); $unfilteredSchema = $mcpHandler->getToolsSchemaUnfiltered(); // Build grouped lists from the schema metadata set by getToolsSchemaUnfiltered() $allDiscoveredTools = array(); // names of all non-system tools $groupedNormalTools = array(); // non-system tools grouped by class_name $groupedSystemTools = array(); // system tools grouped by class_name if (!empty($unfilteredSchema) && is_array($unfilteredSchema)) { foreach ($unfilteredSchema as $def) { $name = $def['name']; $isSystem = !empty($def['is_system']); $classNameKey = !empty($def['class_name']) ? (string) $def['class_name'] : 'DefaultTools'; if (!$isSystem) { $allDiscoveredTools[] = $name; $groupedNormalTools[$classNameKey][] = $def; } else { $groupedSystemTools[$classNameKey][] = $def; } } } $rawAst = getDolGlobalString('AI_ASSISTANT_ALLOWED_TOOLS'); $rawMcp = getDolGlobalString('AI_MCP_SERVER_ALLOWED_TOOLS'); $astAllowed = McpHandler::resolveAllowList($rawAst, $allDiscoveredTools); $mcpAllowed = McpHandler::resolveAllowList($rawMcp, $allDiscoveredTools); /** * ACTIONS */ if ($action == 'addrights' || $action == 'delrights') { if (GETPOST('token', 'alpha') !== $_SESSION['token']) { accessforbidden('Bad token'); } if (!empty($toolcontext) && !empty($toolname)) { if ($toolcontext === 'ast') { if ($action == 'addrights') { if (!in_array($toolname, $astAllowed, true)) { $astAllowed[] = $toolname; } } else { $astAllowed = array_values(array_diff($astAllowed, array($toolname))); } $val = empty($astAllowed) ? 'NONE' : implode(',', $astAllowed); dolibarr_set_const($db, 'AI_ASSISTANT_ALLOWED_TOOLS', $val, 'chaine', 0, '', $conf->entity); } if ($toolcontext === 'mcp') { if ($action == 'addrights') { if (!in_array($toolname, $mcpAllowed, true)) { $mcpAllowed[] = $toolname; } } else { $mcpAllowed = array_values(array_diff($mcpAllowed, array($toolname))); } $val = empty($mcpAllowed) ? 'NONE' : implode(',', $mcpAllowed); dolibarr_set_const($db, 'AI_MCP_SERVER_ALLOWED_TOOLS', $val, 'chaine', 0, '', $conf->entity); } } header('Location: ' . $_SERVER['PHP_SELF']); exit; } if ($action == 'apply_preset' && !empty($toolcontext) && !empty($mode)) { if (GETPOST('token', 'alpha') !== $_SESSION['token']) { accessforbidden('Bad token'); } $resultSet = array(); if ($mode === 'all') { $resultSet = $allDiscoveredTools; } elseif ($mode === 'none') { $resultSet = array(); } elseif ($mode === 'readonly') { foreach ($allDiscoveredTools as $tName) { // Tools starting with a mutating verb are write tools. // This heuristic matches the naming convention used throughout ai/tools/*. // External tool authors should follow the same convention. if (!preg_match('/^(create|update|delete|add|remove|change|write|edit|validate|pay|send)/i', $tName)) { $resultSet[] = $tName; } } } $val = empty($resultSet) ? 'NONE' : implode(',', $resultSet); $constTarget = ($toolcontext === 'mcp') ? 'AI_MCP_SERVER_ALLOWED_TOOLS' : 'AI_ASSISTANT_ALLOWED_TOOLS'; dolibarr_set_const($db, $constTarget, $val, 'chaine', 0, '', $conf->entity); header('Location: ' . $_SERVER['PHP_SELF']); exit; } /* * VIEW */ $help_url = ''; $title = 'AiSetup'; llxHeader('', $langs->trans($title), '', '', 0, 0, array(dol_buildpath('/ai/js/ai.js', 1)), array(dol_buildpath('/ai/css/ai.css', 1)), '', 'mod-ai page-admin'); $linkback = '' . img_picto($langs->trans("BackToModuleList"), 'back', 'class="pictofixedwidth"') . '' . $langs->trans("BackToModuleList") . ''; print load_fiche_titre($langs->trans($title), $linkback, 'title_setup'); $head = aiAdminPrepareHead(); print dol_get_fiche_head($head, 'tools', 'MCP Server', -1, 'ai'); print '' . $langs->trans("ToolAccessControlHelp") . '

'; print '
'; // Presets For Chat Assistant print '
'; print '' . $langs->trans('PresetsForChatAssistant') . ':
'; print '' . $langs->trans('AllTools') . ''; print '' . $langs->trans('ViewOnly') . ''; print '' . $langs->trans('None') . ''; print '
'; // Presets For MCP Server print '
'; print '' . $langs->trans('PresetsForMcpServer') . ':
'; print '' . $langs->trans('AllTools') . ''; print '' . $langs->trans('ViewOnly') . ''; print '' . $langs->trans('None') . ''; print '
'; print '
'; // Tools table print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; if (empty($groupedNormalTools) && empty($groupedSystemTools)) { print ''; } else { $groupId = 0; $finalGroupsList = array_merge($groupedNormalTools, $groupedSystemTools); foreach ($finalGroupsList as $categoryName => $definitions) { $groupId++; print ''; print ''; print ''; foreach ($definitions as $def) { $name = $def['name']; $desc = !empty($def['description']) ? $def['description'] : '-'; $isSystem = !empty($def['is_system']); $isAstOn = $isSystem ? true : in_array($name, $astAllowed, true); $isMcpOn = $isSystem ? true : in_array($name, $mcpAllowed, true); if (preg_match('/^(create|update|delete|add|remove|change|write|edit|validate|pay|send)/i', $name)) { $type = 'write'; $typeBadge = '' . $langs->trans('Modify') . ''; } else { $type = 'read'; $typeBadge = '' . $langs->trans('ViewOnly') . ''; } print ''; // Tool print ''; // Type Badge print ''; // Tool Description (hidden on mobile) print ''; $lockCssClass = $isSystem ? ' opacitymedium disabled' : ''; // Chat Assistant Switch print ''; // MCP Server Switch print ''; print '' . "\n"; } } } print '
' . $langs->trans('Tool') . '' . $langs->trans('ToolActionType') . '' . $langs->trans('ToolDescription') . '' . $langs->trans('ChatAssistant') . '' . $langs->trans('McpServer') . '
' . $langs->trans('NoMcpToolsDiscovered') . '
'; print '▼ ' . dol_escape_htmltag($categoryName); print '
'; print '' . dol_escape_htmltag($name) . ''; if ($isSystem) { print '
' . $langs->trans('System') . ''; } print '
' . $typeBadge . '' . dol_escape_htmltag($desc) . ''; if ($isSystem) { print img_picto($langs->trans('Active'), 'switch_on', '', 0, 0, 0, '', 'opacitymedium'); } else { $actAst = $isAstOn ? 'delrights' : 'addrights'; $pictoAst = $isAstOn ? 'switch_on'.($type == 'write' ? '_warning' : '') : 'switch_off'; $urlAst = dolBuildUrl($_SERVER["PHP_SELF"], [ 'action' => $actAst, 'toolcontext' => 'ast', 'toolname' => $name ], true); print ''; print img_picto($langs->trans($isAstOn ? 'Remove' : 'Add'), $pictoAst, '', 0, 0, 0, '', !$isAstOn ? 'font-status6' : ($type == 'write' ? 'font-status2' : 'font-status4')); print ''; } print ''; if ($isSystem) { print img_picto($langs->trans('Active'), 'switch_on', '', 0, 0, 0, '', 'opacitymedium'); } else { $actMcp = $isMcpOn ? 'delrights' : 'addrights'; $pictoMcp = $isMcpOn ? 'switch_on'.($type == 'write' ? '_warning' : '') : 'switch_off'; $urlMcp = dolBuildUrl($_SERVER["PHP_SELF"], [ 'action' => $actMcp, 'toolcontext' => 'mcp', 'toolname' => $name ], true); print ''; print img_picto($langs->trans($isMcpOn ? 'Remove' : 'Add'), $pictoMcp, '', 0, 0, 0, '', !$isMcpOn ? 'font-status6' : ($type == 'write' ? 'font-status2' : 'font-status4')); print ''; } print '
'; print '
'; print dol_get_fiche_end(); llxFooter(); $db->close();